Imports System.Windows.Forms, System.Drawing

'this is a custom class to extend the functionality of the C1Component Flexgrid control
'to use it, insert a normal flexgrid and then change the type to MyFlexGrid in the Windows
'Form Design generated code section
''' <summary>
''' This class adds functionality to a DataGridView; to use it, instantiate the class by passing the instance of the
''' datagrid view.
''' </summary>
Public Class DGVEditor

#Region "Events"


    ''' <summary>
    ''' Event is raised after context menu action is carried out; the event argument indicates what command was completed
    ''' </summary>
    Public Event AfterContextMenu(ByVal sender As Object, ByVal e As ContextMenuEventArgs)

    ''' <summary>
    ''' Event is raised before context menu action is carried out; the event argument indicates what command is about to be carried out
    ''' </summary>
    Public Event BeforeContextMenu(ByVal sender As Object, ByVal e As ContextMenuEventArgs)

    ''' <summary>
    ''' Event is raised after context menu or other editing has changed grid contents
    ''' </summary>
    Public Event HasBeenModified(ByVal sender As Object, ByVal e As System.EventArgs)

    ''' <summary>
    ''' Event is raised as background events progress
    ''' </summary>
    Public Event ProgressChanged(ByVal sender As Object, ByVal e As ProgressEventArgs)

    ''' <summary>
    ''' Event is raised when background event is complete
    ''' </summary>
    Public Event ProgressComplete(ByVal sender As Object)

    ''' <summary>
    ''' Event is raised after a group of cells is selected and the mouse is lifted; for certain interactive grids will be more efficient 
    ''' to handle this event than the native event, because the mouseup event causes all cells to be reselected and it can be really slow
    ''' </summary>
    Public Event SelectionChanged(ByVal sender As Object)

#End Region

#Region "Functions to determine range of selected cells"

    Private Function FirstCellValue() As Object
        Return dgv.Item(MinColSel, MinRowSel).Value
    End Function

    Private Function MaxColSel() As Integer
        If dgv.RowCount = 0 OrElse dgv.SelectedCells.Count = 0 Then Return -1
        Dim Max As Integer = Integer.MinValue
        For Each c As DataGridViewCell In dgv.SelectedCells
            If c.ColumnIndex > Max AndAlso c.Visible Then Max = c.ColumnIndex
        Next
        Return Max
    End Function

    Private Function MaxRowSel() As Integer
        If dgv.RowCount = 0 OrElse dgv.SelectedCells.Count = 0 Then Return -1
        Dim Max As Integer = Integer.MinValue
        For Each c As DataGridViewCell In dgv.SelectedCells
            If c.RowIndex > Max Then Max = c.RowIndex
        Next
        Max = Math.Min(Max, dgv.RowCount - IIf(dgv.AllowUserToAddRows, 2, 1))
        Return Max
    End Function

    Private Function MinColSel() As Integer
        If dgv.RowCount = 0 OrElse dgv.SelectedCells.Count = 0 Then Return 0
        Dim Min As Integer = Integer.MaxValue
        For Each c As DataGridViewCell In dgv.SelectedCells
            If c.ColumnIndex < Min AndAlso c.Visible Then Min = c.ColumnIndex
        Next
        Return Min
    End Function
    Private Function MinRowSel() As Integer
        If dgv.RowCount = 0 OrElse dgv.SelectedCells.Count = 0 Then Return 0
        Dim Min As Integer = Integer.MaxValue
        For Each c As DataGridViewCell In dgv.SelectedCells
            If c.RowIndex < Min Then Min = c.RowIndex
        Next
        Return Min
    End Function
#End Region

    ''' <summary>
    ''' Information about column validation; will be stored in column tag if used
    ''' </summary>
    Private Class clsValidation
        Friend AllowNull As Boolean = False
        Friend MaxRange As Object = Nothing
        Friend MaxValue As Object = Nothing
        Friend MinRange As Object = Nothing
        Friend MinValue As Object = Nothing
        Friend ValidationType As Type
        Friend Sub New(ByVal _ValidationType As Type, ByVal _AllowNull As Boolean)
            ValidationType = _ValidationType
            AllowNull = _AllowNull
        End Sub
        Friend Sub New(ByVal _ValidationType As Type, ByVal _AllowNull As Boolean, ByVal _MinValue As Object, ByVal _MaxValue As Object)
            ValidationType = _ValidationType
            AllowNull = _AllowNull
            MinValue = _MinValue
            MaxValue = _MaxValue
        End Sub
        Friend Sub New(ByVal _ValidationType As Type, ByVal _AllowNull As Boolean, ByVal _MinValue As Object, ByVal _MaxValue As Object, ByVal _MinRange As Object, ByVal _MaxRange As Object)
            ValidationType = _ValidationType
            AllowNull = _AllowNull
            MinValue = _MinValue
            MaxValue = _MaxValue
            MinRange = _MinRange
            MaxRange = _MaxRange
        End Sub
    End Class

    Private Structure structUndo
        Dim Column As Integer
        Dim Row As Integer
        Dim Value As Object
        Dim Action As enumUndoAction
        Dim UndoID As Integer
    End Structure

    Public Enum enumColumnTypes
        None
        ComboBox
        ComboBoxEdit
        CheckBox
        Calendar
        Link
        Button
        ColorPicker
    End Enum

    ''' <summary>Setting which describes how wide reports are handled for printing</summary>
    Public Enum enumReportScaling
        ''' <summary>No adjustments are made</summary>
        None
        ''' <summary>Narrow reports have columns made wider</summary>
        Stretched
        ''' <summary>Wide columns use word-wrap</summary>
        Multiline
        ''' <summary>Fonts are reduced in size</summary>
        ScaledFonts
    End Enum

    Public Enum enumUndoAction
        ChangeCell
        InsertRow
        InsertColumn
        DeleteRow
        DeleteColumn
    End Enum

    'allow me to save to excel in background thread
    Private WithEvents bwExcel As System.ComponentModel.BackgroundWorker

    Private WithEvents dgv As DataGridView

    'these are used to make the selection of multiple cells using the mouse behave more like Excel, 
    'where the first cell clicked on remains the active cell after all are selected
    'it also allows the active cell to be highlighted differently than the rest of the selected cells
    Private LastCurrentCell As DataGridViewCell

    Private m_AllowRowChange As Boolean = True

    Private m_AutoFillSelectedCells As Boolean

    Private m_AutoNumberRows As Boolean = False

    Private m_IsModified As Boolean

    Private WithEvents mnu As ContextMenuStrip
    Private MouseDownCell As DataGridViewCell
    Private printdoc As Drawing.Printing.PrintDocument = Nothing
    Private ReportScaling As enumReportScaling
    Private undoItem As structUndo

    Private UndoList As New Generic.Stack(Of structUndo)

    Public Sub New(ByRef _dgv As DataGridView, Optional ByRef _GridFont As Drawing.Font = Nothing, Optional ByRef _ReportScaling As enumReportScaling = enumReportScaling.None, Optional ByRef _printDoc As Drawing.Printing.PrintDocument = Nothing)
        If Not IsDesignMode(_dgv) Then Call Initialize(_dgv, _GridFont, _ReportScaling, _printDoc)
    End Sub

    Public Sub New()
    End Sub

    ''' <summary>
    ''' Launch the program associated with this file type and load the file
    ''' </summary>
    ''' <param name="FileName">Name of document to open</param>
    ''' <returns>False if file not found or appropriate application is missing</returns>
    ''' <remarks>Not used at this time--just leave Excel open and visible</remarks>
    Private Function LaunchFile(ByVal FileName As String) As Boolean
        Try
            If Dir(FileName) = "" Then MessageBox.Show("Desired file not found: " & FileName, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) : Return False
            System.Diagnostics.Process.Start(FileName)
            Return True
        Catch ex As Exception
            MessageBox.Show("You must have the appropriate application installed on your computer to view the document: " & _
                FileName & vbCr & vbCr & "For example, to view a PDF document, you must have Adobe Acrobat Reader installed.", _
                "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
            Return False
        End Try
    End Function

    Private Sub AddMnuItem(ByVal Text As String, ByVal Image As System.Drawing.Image, ByVal Name As String, ByVal ShortcutKeys As Keys)
        With mnu.Items
            Dim ts As New ToolStripMenuItem
            With ts
                .Text = Text
                .Image = Image
                .ImageScaling = ToolStripItemImageScaling.None

                .Name = Name
                .ShortcutKeys = ShortcutKeys
                '.ShortcutKeyDisplayString = ""
            End With
            .Add(ts)
        End With
    End Sub

    ''' <summary>
    ''' Add another row, same parsing rules as for Headings (~ between cells, | for line break in cell)
    ''' </summary>
    ''' <param name="Value">Delimited string containing values to place into cells</param>
    Public Sub AddRow(ByVal Value As String)
        'add another row, same parsing rules as for Headings
        Dim cells As String() = Value.Split("~")
        dgv.RowCount += 1
        For i As Integer = 0 To cells.Length - 1
            dgv.Item(i, dgv.RowCount - 1).Value = cells(i).Replace("|", vbCr)
        Next
    End Sub

    ''' <summary>
    ''' Add another row, one item per cell
    ''' </summary>
    ''' <param name="Items">Items to place in consecutive cells</param>
    Public Sub AddRowItems(ByVal ParamArray Items() As Object)
        dgv.RowCount += 1
        For i As Integer = 0 To Items.Length - 1
            dgv.Item(i, dgv.RowCount - 1).Value = Items(i)
        Next
    End Sub

    ''' <summary>
    ''' Add an undo item to the list
    ''' </summary>
    ''' <param name="_Column">Column</param>
    ''' <param name="_Row">Row</param>
    ''' <param name="_Action">Type of action taken</param>
    ''' <param name="_UndoID">Unique ID used to group multiple undo actions (if omitted, will assign one and there will be no grouping)--My.Computer.Clock.TickCount works well for this</param>
    ''' <remarks></remarks>
    Public Sub AddUndo(ByVal _Column As Integer, ByVal _Row As Integer, ByVal _Action As DGVEditor.enumUndoAction, Optional ByVal _UndoID As Integer = 0)
        With undoItem
            .Column = _Column
            .Row = _Row
            .Action = _Action
            If _Action = enumUndoAction.ChangeCell Then .Value = dgv.Item(.Column, .Row).Value Else .Value = Nothing
            .UndoID = _UndoID
            If .UndoID = 0 Then .UndoID = My.Computer.Clock.TickCount
        End With
        UndoList.Push(undoItem)
    End Sub

    ''' <summary>
    ''' Add an undo item to the list (will assign ChangeCell action)
    ''' </summary>
    ''' <param name="_Column">Column</param>
    ''' <param name="_Row">Row</param>
    ''' <param name="_UndoID">Unique ID used to group multiple undo actions (if omitted, will assign one and there will be no grouping)--My.Computer.Clock.TickCount works well for this</param>
    ''' <remarks></remarks>
    Public Sub AddUndo(ByVal _Column As Integer, ByVal _Row As Integer, Optional ByVal _UndoID As Integer = 0)
        AddUndo(_Column, _Row, enumUndoAction.ChangeCell, _UndoID)
    End Sub

    ''' <summary>
    ''' Clear undo list preventing future undo operations
    ''' </summary>
    ''' <remarks>This is used when contents of grid get updated (e.g., by dropdown list) and don't want to be able to undo after grid has been filled with different data</remarks>
    Public Sub ClearUndo()
        UndoList.Clear()
    End Sub

    Private Sub AutoFill(ByVal NewValue As Object)
        Try
            'warning: autofill causes bad behavior when you move from a cell in edit mode to a different cell!!!!!
            If AutoFillSelectedCells AndAlso Not dgv.IsCurrentCellInEditMode Then
                With Me
                    For r As Integer = MinRowSel() To MaxRowSel()
                        For c As Integer = MinColSel() To MaxColSel()
                            dgv.Item(c, r).Value = NewValue
                        Next
                    Next
                End With
                'set the modified flag
                m_IsModified = True
                RaiseEvent HasBeenModified(Me, New System.EventArgs)
            End If
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' Auto resizes the specified columns then makes all width equal to the widest one
    ''' </summary>
    ''' <param name="columnIndex1">First column to adjust</param>
    ''' <param name="columnIndex2">Last column to adjust</param>
    ''' <remarks></remarks>
    Public Sub AutoResizeColumnsEqual(ByVal columnIndex1 As Integer, ByVal columnIndex2 As Integer, Optional ByVal autoSizeColumnMode As DataGridViewAutoSizeColumnMode = DataGridViewAutoSizeColumnMode.AllCells, Optional MinColWidth As String = "")
        Try
            'dgv.SuspendLayout()
            Dim maxWidth As Integer = dgv.CreateGraphics.MeasureString(MinColWidth, dgv.DefaultCellStyle.Font, 1000).Width
            For c As Integer = columnIndex1 To columnIndex2
                If dgv.Columns(c).Visible Then
                    dgv.AutoResizeColumn(c, autoSizeColumnMode)
                    maxWidth = Math.Max(maxWidth, dgv.Columns(c).Width)
                End If
            Next
            For c As Integer = columnIndex1 To columnIndex2
                If dgv.Columns(c).Visible Then
                    dgv.Columns(c).Width = maxWidth
                End If
            Next
            'dgv.ResumeLayout(True)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub bwExcel_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bwExcel.DoWork
        Try
            With New WRDB.MSExcel.clsExcel() 'e.Argument)
                AddHandler .ProgressChanged, AddressOf ProgressChangedHandler
                .ExportDataGridView(dgv)
                RemoveHandler .ProgressChanged, AddressOf ProgressChangedHandler
            End With
        Catch ex As Exception
            ErrorMsg("Unable to connect to Excel libraries on your computer; this feature will only work if you have a compatible version of Excel already installed on your computer.", ex)
        End Try
    End Sub

    ''' <summary>
    ''' Send message from background worker up through this class
    ''' </summary>
    Private Sub bwExcel_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles bwExcel.ProgressChanged
        RaiseEvent ProgressChanged(Me, New ProgressEventArgs(e.ProgressPercentage, False))
    End Sub

    Private Sub bwExcel_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bwExcel.RunWorkerCompleted
        RaiseEvent ProgressComplete(Me)
        bwExcel.Dispose()
        bwExcel = Nothing
    End Sub

    Public Sub CancelOperation()
        If bwExcel IsNot Nothing AndAlso bwExcel.IsBusy Then bwExcel.CancelAsync()
    End Sub

    ''' <summary>
    ''' Clear contents of grid not including row ad column headings
    ''' </summary>
    Public Sub ClearData()
        With dgv
            For r As Integer = 0 To .RowCount - 1
                For c As Integer = 0 To .ColumnCount - 1
                    .Item(c, r).Value = Nothing
                Next
            Next
        End With
        IsModified = False
    End Sub

    ''' <summary>
    ''' this will allow all the selected cells to take on the value of the cell just edited
    ''' </summary>
    Private Sub dgv_CellEndEdit(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles dgv.CellEndEdit
        'autofill doesn't work on boolean
        If dgv.Columns(e.ColumnIndex).ValueType IsNot GetType(Boolean) Then AutoFill(dgv.Item(e.ColumnIndex, e.RowIndex).Value)
        dgv.Rows(e.RowIndex).ErrorText = String.Empty
        m_IsModified = True
        RaiseEvent HasBeenModified(Me, New System.EventArgs)
    End Sub

    Private Sub dgv_CellFormatting(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellFormattingEventArgs) Handles dgv.CellFormatting
        Try
            With dgv
                'don't format if cell is not visible
                If Not .Visible OrElse
                    e.ColumnIndex < .FirstDisplayedScrollingColumnIndex OrElse e.ColumnIndex > .FirstDisplayedScrollingColumnIndex + .DisplayedColumnCount(True) OrElse
                    e.RowIndex < .FirstDisplayedScrollingRowIndex OrElse e.RowIndex > .FirstDisplayedScrollingRowIndex + .DisplayedRowCount(True) Then Exit Sub

                ValidateCell(e.ColumnIndex, e.RowIndex, e.CellStyle, e.Value)
                'following required to make links adopt column color
                If e.ColumnIndex <> -1 AndAlso (TypeOf .Columns(e.ColumnIndex) Is DataGridViewLinkColumn OrElse TypeOf .Item(e.ColumnIndex, e.RowIndex) Is DataGridViewLinkCell) Then
                    If .Item(e.ColumnIndex, e.RowIndex).Selected Then
                        CType(.Item(e.ColumnIndex, e.RowIndex), DataGridViewLinkCell).LinkColor = Color.White
                    Else
                        CType(.Item(e.ColumnIndex, e.RowIndex), DataGridViewLinkCell).LinkColor = Color.Blue
                    End If
                End If
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    'cannot do the following because in some applications when cell set programmatically the tabs just keep getting added to the buffer and the control eventually just goes crazy!
    'replaced with SelectionChanged approach
    ' ''' <summary>
    ' ''' Prevent cursor from going into read-only cells by moving to the right
    ' ''' </summary>
    'Private Sub dgv_CellEnter(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles dgv.CellEnter
    '    With dgv
    '        If .Visible AndAlso Not .ReadOnly AndAlso .Item(e.ColumnIndex, e.RowIndex).ReadOnly AndAlso Not TypeOf .Item(e.ColumnIndex, e.RowIndex) Is DataGridViewLinkCell Then
    '            SendKeys.Send("<TAB>")
    '        End If
    '    End With
    'End Sub

    Private Sub dgv_CellValidating(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) Handles dgv.CellValidating
        Try
            With dgv
                If Not .Visible Then Exit Sub
                If .CurrentCell.IsInEditMode Then AddUndo(e.ColumnIndex, e.RowIndex, enumUndoAction.ChangeCell)

                ValidateCell(e.ColumnIndex, e.RowIndex, .Item(e.ColumnIndex, e.RowIndex).Style, e.FormattedValue)
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub dgv_ColumnHeaderMouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellMouseEventArgs) Handles dgv.ColumnHeaderMouseClick
        Try
            With dgv
                'no columns must be sortable
                Dim Sortable As Boolean = False
                For c As Integer = 0 To dgv.ColumnCount - 1
                    If .Columns(c).SortMode <> DataGridViewColumnSortMode.NotSortable Then Sortable = True : Exit For
                Next
                If Not Sortable Then
                    'find first visible row
                    For r As Integer = 0 To dgv.RowCount - 1
                        If dgv.Rows(r).Visible Then
                            .CurrentCell = .Item(e.ColumnIndex, r)
                            If LastCurrentCell IsNot Nothing Then LastCurrentCell.Style.SelectionForeColor = Drawing.Color.White
                            If .CurrentCell IsNot Nothing Then .CurrentCell.Style.SelectionForeColor = Drawing.Color.Red
                            LastCurrentCell = .CurrentCell
                            Exit For
                        End If
                    Next
                    .SelectionMode = DataGridViewSelectionMode.ColumnHeaderSelect
                    .Columns(e.ColumnIndex).Selected = True
                End If
            End With
            RaiseEvent SelectionChanged(dgv)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub dgv_DataError(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewDataErrorEventArgs) Handles dgv.DataError
        With dgv
            If Not .Visible Then Exit Sub
            Debug.Print(String.Format("Invalid data was entered in column '{0}', row {2}; {1}", .Columns(e.ColumnIndex).HeaderText, e.Exception.Message, e.RowIndex))
            If Not .Item(e.ColumnIndex, e.RowIndex).ValueType Is GetType(Byte()) Then Debug.Print(TestNull(.Item(e.ColumnIndex, e.RowIndex).Value, "Null Value"))
            'Debug.Print(dgv.Columns("Size").ValueType.ToString)
            .Rows(e.RowIndex).ErrorText = String.Format("Invalid data was entered in column '{0}'; correct entry or press ESC to cancel edit", .Columns(e.ColumnIndex).HeaderText)
        End With
    End Sub

    ''' <summary>
    ''' Force context menu to appear even if in edit mode
    ''' </summary>
    Private Sub dgv_EditingControlShowing(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles dgv.EditingControlShowing
        Try
            e.Control.ContextMenuStrip = mnu

            'add new handler so can capture keypresses in editor and convert . to , if international format

            If TypeOf e.Control Is DataGridViewTextBoxEditingControl Then
                With CType(e.Control, DataGridViewTextBoxEditingControl)
                    RemoveHandler .KeyPress, AddressOf Editor_KeyPress
                    AddHandler .KeyPress, AddressOf Editor_KeyPress
                End With
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub Editor_KeyPress(sender As Object, e As KeyPressEventArgs)
        With dgv
            If e.KeyChar = "." AndAlso
               Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator <> "." AndAlso
               (AutoReplaceDecimalSeparator = enumAutoReplaceDecimalSeparator.AllCells OrElse .CurrentCell.ValueType = GetType(Double) OrElse .CurrentCell.ValueType = GetType(Single)) Then
                e.KeyChar = Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator
                e.Handled = False
            End If
        End With
    End Sub

    Private Sub dgv_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles dgv.KeyDown
        Try
            With dgv
                If e.Alt AndAlso e.KeyCode = Keys.Back Then Undo() : e.Handled = True
                If Not .ReadOnly Then
                    If e.Control AndAlso e.KeyCode = Keys.Add Then mnu.Items("Insert").PerformClick() : e.Handled = True
                    If e.Control AndAlso e.KeyCode = Keys.Subtract Then mnu.Items("DeleteRows").PerformClick() : e.Handled = True
                End If
                If e.Shift AndAlso e.KeyCode = Keys.Space Then
                    .CurrentCell = .Item(0, .CurrentCellAddress.Y)
                    .ClearSelection()
                    .SelectionMode = DataGridViewSelectionMode.FullRowSelect
                    .Rows(dgv.CurrentCellAddress.Y).Selected = True
                    e.Handled = True
                End If
                If e.Control AndAlso e.KeyCode = Keys.Space Then
                    .CurrentCell = .Item(.CurrentCellAddress.X, 0)
                    .ClearSelection()
                    .SelectionMode = DataGridViewSelectionMode.FullColumnSelect
                    .Columns(dgv.CurrentCellAddress.X).Selected = True
                    e.Handled = True
                End If
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    'note: the mousedown/up trick for the left click is necessary so when selecting range of cells, the first cell clicked will be set as the current cell (usually at the top).

    Private Sub dgv_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles dgv.MouseDown
        Try
            Dim hti As DataGridView.HitTestInfo = sender.HitTest(e.X, e.Y)
            If hti.Type <> DataGridViewHitTestType.Cell Then Exit Sub
            If e.Button = MouseButtons.Right Then
                With dgv
                    If Not .Item(hti.ColumnIndex, hti.RowIndex).Selected Then
                        ' User right clicked a row that is not selected, so throw away all other selections and select this row
                        .ClearSelection()
                        .Item(hti.ColumnIndex, hti.RowIndex).Selected = True
                        .CurrentCell = .Item(hti.ColumnIndex, hti.RowIndex)
                    End If
                End With
            ElseIf e.Button = MouseButtons.Left Then
                MouseDownCell = dgv.Item(hti.ColumnIndex, hti.RowIndex)
            End If
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub dgv_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles dgv.MouseUp
        Try
            If e.Button = MouseButtons.Left AndAlso (MouseDownCell Is Nothing OrElse MouseDownCell.RowIndex <> -1) Then
                Try
                    With dgv
                        Dim hti As DataGridView.HitTestInfo = sender.HitTest(e.X, e.Y)

                        If hti.Type <> DataGridViewHitTestType.Cell AndAlso
                           hti.Type <> DataGridViewHitTestType.TopLeftHeader Then Exit Sub

                        If hti.Type = DataGridViewHitTestType.TopLeftHeader Then
                            .CurrentCell = .Item(0, 0)
                            .SelectAll()
                        Else
                            Dim sel As DataGridViewSelectedCellCollection = .SelectedCells
                            .CurrentCell = MouseDownCell
                            For Each c As DataGridViewCell In sel
                                c.Selected = True
                            Next
                        End If
                        If LastCurrentCell IsNot Nothing Then LastCurrentCell.Style.SelectionForeColor = Drawing.Color.White
                        If .CurrentCell IsNot Nothing Then .CurrentCell.Style.SelectionForeColor = Drawing.Color.Red
                        LastCurrentCell = .CurrentCell
                        RaiseEvent SelectionChanged(dgv)
                    End With
                Catch ex As Exception
                    'may fail if couldn't validate cell/row
                End Try
            End If
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub dgv_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles dgv.Resize, dgv.VisibleChanged
        UpdateRowHeaders()
    End Sub

    Private Sub dgv_RowHeaderMouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellMouseEventArgs) Handles dgv.RowHeaderMouseClick
        Try
            With dgv
                For c As Integer = 0 To dgv.ColumnCount - 1
                    If .Columns(c).Visible Then
                        .CurrentCell = .Item(c, e.RowIndex)
                        If LastCurrentCell IsNot Nothing Then LastCurrentCell.Style.SelectionForeColor = Drawing.Color.White
                        If .CurrentCell IsNot Nothing Then .CurrentCell.Style.SelectionForeColor = Drawing.Color.Red
                        LastCurrentCell = .CurrentCell
                        Exit For
                    End If
                Next
                .SelectionMode = DataGridViewSelectionMode.RowHeaderSelect
                .Rows(e.RowIndex).Selected = True
            End With
            RaiseEvent SelectionChanged(dgv)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub dgv_Scroll(ByVal sender As Object, ByVal e As System.Windows.Forms.ScrollEventArgs) Handles dgv.Scroll
        If dgv.Visible Then UpdateRowHeaders()
    End Sub

    'Private Sub dgv_CurrentCellChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles dgv.CurrentCellChanged
    '    'With dgv
    '    '    .SelectionMode = DataGridViewSelectionMode.CellSelect
    '    '    If .CurrentCell IsNot Nothing Then
    '    '        .CurrentCell.Selected = True
    '    '        If LastCurrentCell IsNot Nothing Then LastCurrentCell.Style.SelectionForeColor = Drawing.Color.White
    '    '        If .CurrentCell IsNot Nothing Then .CurrentCell.Style.SelectionForeColor = Drawing.Color.Red
    '    '        LastCurrentCell = .CurrentCell
    '    '    End If
    '    'End With
    'End Sub

    ''' <summary>
    ''' Prevent focus from going to readonly cell in grid
    ''' </summary>
    Private Sub dgv_SelectionChanged(sender As Object, e As System.EventArgs) Handles dgv.SelectionChanged
        With dgv
            If Not .Visible OrElse .CurrentCell Is Nothing Then Exit Sub
            Dim nextRow As Integer = .CurrentCell.RowIndex
            Dim nextCol As Integer = .CurrentCell.ColumnIndex
            While .Visible AndAlso Not .ReadOnly AndAlso .CurrentCell IsNot Nothing AndAlso .Item(nextCol, nextRow).ReadOnly AndAlso Not .Item(nextCol, nextRow).Visible  'AndAlso Not TypeOf .Item(nextCol, nextRow) Is DataGridViewLinkCell
                nextCol += 1
                If nextCol > .ColumnCount - 1 Then
                    nextCol = 0
                    nextRow += 1
                End If
                If nextRow > .RowCount - 1 Then Exit Sub
            End While
            .CurrentCell = .Item(nextCol, nextRow)
        End With
    End Sub

    Public Sub Initialize(ByRef _dgv As DataGridView, Optional ByRef _GridFont As Drawing.Font = Nothing, Optional ByRef _ReportScaling As DGVEditor.enumReportScaling = DGVEditor.enumReportScaling.None, Optional ByRef _printDoc As Drawing.Printing.PrintDocument = Nothing)
        Try
            dgv = _dgv
            If _GridFont Is Nothing Then _GridFont = New System.Drawing.Font("Tahoma", 8)

            dgv.DefaultCellStyle.Font = _GridFont
            dgv.Font = _GridFont
            dgv.DefaultCellStyle.ForeColor = Drawing.Color.Blue
            dgv.DefaultCellStyle.SelectionBackColor = Drawing.Color.Blue
            dgv.DefaultCellStyle.SelectionForeColor = Drawing.Color.White
            'dgv.RowHeadersDefaultCellStyle.Font = _GridFont
            'dgv.ColumnHeadersDefaultCellStyle.Font = _GridFont
            ReportScaling = _ReportScaling
            printdoc = _printDoc

            'set default properties to make all my datagridviews work similarly
            With dgv
                .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText 'required for clipboard usage
                .EditMode = DataGridViewEditMode.EditOnKeystrokeOrF2 'allow cell easy editing (will change if comboboxes used)
                .AllowUserToAddRows = False 'don't show empty row at the bottom
                .AllowUserToDeleteRows = False 'make user use context menu to add/delete rows
                For Each c As DataGridViewColumn In .Columns
                    c.SortMode = DataGridViewColumnSortMode.NotSortable 'don't allow column sorting
                Next

            End With

            'create context menu
            mnu = New ContextMenuStrip()
            With mnu.Items
                AddMnuItem("Cut", My.Resources.CutHS, "Cut", Keys.Control Or Keys.X)
                AddMnuItem("Copy", My.Resources.CopyHS, "Copy", Keys.Control Or Keys.C)
                AddMnuItem("Paste", My.Resources.PasteHS, "Paste", Keys.Control Or Keys.V)
                AddMnuItem("Delete", My.Resources.DeleteHS, "Delete", Keys.Delete)
                AddMnuItem("Select All", My.Resources.ShowGridlinesHS, "SelectAll", Nothing)
                AddMnuItem("Undo Edit", My.Resources.Edit_UndoHS, "Undo", Keys.Control Or Keys.Z)
                .Add(New ToolStripSeparator)
                AddMnuItem("Find...", My.Resources.FindHS, "Find", Keys.Control Or Keys.F)
                AddMnuItem("Find Next", My.Resources.FindNextHS, "FindNext", Keys.F3)
                AddMnuItem("Search && Replace...", My.Resources.SychronizeListHS, "Replace", Nothing)
                AddMnuItem("Sort by Column", My.Resources.SortHS, "Sort", Nothing)
                .Add(New ToolStripSeparator)
                AddMnuItem("Fill", My.Resources.FillDownHS, "Fill", Nothing)
                AddMnuItem("Series...", My.Resources.List_NumberedHS, "Series", Nothing)
                AddMnuItem("Multiply...", My.Resources.CalculatorHS, "Multiply", Nothing)
                AddMnuItem("Add...", My.Resources.AddTableHS, "Add", Nothing)
                .Add(New ToolStripSeparator)
                AddMnuItem("Append Rows...", My.Resources.TileWindowsHorizontallyHS, "Append", Nothing)
                AddMnuItem("Insert Rows", My.Resources.IndentHS, "Insert", Nothing)
                AddMnuItem("Duplicate Row", My.Resources.DoubleRightArrowHS, "DuplicateRow", Nothing)
                AddMnuItem("Delete Rows", My.Resources.OutdentHS, "DeleteRows", Nothing)
                AddMnuItem("Clear Table", My.Resources.DeleteFolderHS, "ClearTable", Nothing)
                .Add(New ToolStripSeparator)
                AddMnuItem("Display Statistics...", My.Resources.CalculatorHS, "Stats", Nothing)
                .Add(New ToolStripSeparator)
                AddMnuItem("Export to Excel...", My.Resources.Excel, "Excel", Nothing)
                If printdoc IsNot Nothing Then AddMnuItem("Pri&nt...", My.Resources.PrintHS, "Print", Nothing)
                AddMnuItem("Create PDF...", My.Resources.PDF, "PDF", Nothing)
            End With

            dgv.ContextMenuStrip = mnu
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Public Sub iSetColumnFormat(ByVal ColumnType As enumColumnTypes, ByVal col As Integer, Optional ByVal ComboItems As Generic.List(Of String) = Nothing)
        SetColumnFormat(ColumnType, col, ComboItems)
    End Sub

    ''' <summary>
    ''' This is the real workhorse of the class, where context menu actions are carried out
    ''' </summary>
    Private Sub mnu_ItemClicked(ByVal sender As Object, ByVal e As System.Windows.Forms.ToolStripItemClickedEventArgs) Handles mnu.ItemClicked
        Dim AllowAdd As Boolean = dgv.AllowUserToAddRows
        Try
            mnu.Close() 'so it isn't in the way of other dialogs

            Dim s As String
            Dim name As String = e.ClickedItem.Name
            Static sFind As String = ""

            Dim cm As New ContextMenuEventArgs

            'figure out which event argument to pass back to caller...
            If name = "Fill" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Fill
            ElseIf name = "Cut" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Cut
            ElseIf name = "Copy" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Copy
            ElseIf name = "Paste" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Paste
            ElseIf name = "Delete" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Delete
            ElseIf name = "SelectAll" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.SelectAll
            ElseIf name = "Undo" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Undo
            ElseIf name Like "Find" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Find
            ElseIf name Like "Search" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Replace
            ElseIf name = "Add" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Add
            ElseIf name = "Multiply" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Multiply
            ElseIf name = "Series" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Series
            ElseIf name = "Append" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.AppendRows
            ElseIf name = "Insert" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.InsertRows
            ElseIf name = "DuplicateRow" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.DuplicateRow
            ElseIf name = "DeleteRows" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.DeleteRows
            ElseIf name = "ClearTable" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.ClearTable
            ElseIf name = "Print" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Print
            ElseIf name = "PDF" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.PDF
            ElseIf name Like "Excel" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Excel
            ElseIf name = "Stats" Then
                cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.Stats
            End If

            Dim minrow As Integer = MinRowSel()
            Dim mincol As Integer = MinColSel()
            Dim maxrow As Integer = MaxRowSel()
            Dim maxcol As Integer = MaxColSel()

            cm.RowIndex = minrow
            cm.RowCount = maxrow - minrow + 1

            'send event, and cancel if told to do so
            RaiseEvent BeforeContextMenu(Me, cm)
            If cm.Cancel Then Exit Sub

            Dim UndoID As Integer = My.Computer.Clock.TickCount

            'now, process the command
            With dgv

                .AllowUserToAddRows = False

                If .IsCurrentCellInEditMode Then
                    If name = "Delete" Then 'must manually handle delete key action
                        If TypeOf (.EditingControl) Is TextBox Then
                            With CType(.EditingControl, TextBox)
                                If .SelectedText = "" Then
                                    Dim selstart As Integer = .SelectionStart
                                    If selstart <= .Text.Length - 1 Then
                                        .Text = .Text.Remove(selstart, 1)
                                        .SelectionStart = selstart
                                    End If
                                Else
                                    .SelectedText = ""
                                End If
                            End With
                        End If
                        Exit Sub
                    Else
                        .EndEdit()
                    End If
                End If

                If name = "Fill" Then
                    Application.UseWaitCursor = True
                    Dim fcv As Object = FirstCellValue()
                    For Each c As DataGridViewCell In .SelectedCells
                        If Not c.ReadOnly Then
                            AddUndo(c.ColumnIndex, c.RowIndex, enumUndoAction.ChangeCell, UndoID)
                            c.Value = fcv
                        End If
                    Next
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "Cut" Then
                    Application.UseWaitCursor = True
                    .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText
                    Clipboard.SetDataObject(.GetClipboardContent)
                    For Each c As DataGridViewCell In .SelectedCells
                        If Not c.ReadOnly Then
                            AddUndo(c.ColumnIndex, c.RowIndex, enumUndoAction.ChangeCell, UndoID)
                            'If .Columns(c.ColumnIndex).ValueType Is GetType(Double) Or .Columns(c.ColumnIndex).ValueType Is GetType(Single) Or .Columns(c.ColumnIndex).ValueType Is GetType(Integer) Then
                            '    c.Value = 0
                            'Else
                            '    c.Value = DBNull.Value
                            'End If
                            c.Value = DBNull.Value
                        End If
                    Next
                    RaiseEvent AfterContextMenu(Me, cm)
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "Copy" Then
                    Application.UseWaitCursor = True

                    'create HTML object with headers, text object without
                    Dim obj As New DataObject
                    'problem: in Excel would have to Paste-Special to avoid getting column headers!
                    '.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText
                    'obj.SetData(DataFormats.Html, .GetClipboardContent.GetData(DataFormats.Html, True))

                    'include column headers only if all cells selected
                    'NO! cannot paste back into grid then!

                    'If .SelectedCells.Count = .Rows.Count * .Columns.Count Then 'entire grid is selected
                    '    .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText
                    'Else
                    '    .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText
                    'End If

                    .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText

                    If .GetClipboardContent IsNot Nothing Then
                        With CType(.GetClipboardContent, IDataObject)
                            If .GetDataPresent(DataFormats.Text) Then
                                obj.SetData(DataFormats.Text, .GetData(DataFormats.Text, True))
                                Clipboard.SetDataObject(obj)
                            End If
                        End With
                    End If

                ElseIf name = "Paste" Then
                    Application.UseWaitCursor = True
                    Dim SavedAutoSizeColumnsMode As DataGridViewAutoSizeColumnMode = .AutoSizeColumnsMode
                    .AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None
                    dgv.SuspendLayout()
                    Dim ClipText As String
                    Try
                        ClipText = Clipboard.GetDataObject.GetData(DataFormats.Text, True)
                    Catch ex As Exception
                        MessageBox.Show("Unable to get data from clipboard.")
                        Exit Sub
                    End Try
                    ClipText = ClipText.Replace(vbCrLf, vbCr)
                    Dim Rows() As String = ClipText.Split(New Char() {vbCr}, StringSplitOptions.RemoveEmptyEntries)

                    If .SelectedCells.Count <= 1 OrElse (Rows.Length > 1 OrElse Rows(0).Split(vbTab).Length > 1) Then 'only one cell highlighted or multiple cells in clipboard, let paste spill over to adjacent cells (or expand rows)

                        'get out of edit mode if more than one row or column in clipboard
                        If .IsCurrentCellInEditMode AndAlso Rows.Length > 1 AndAlso Rows(0).Split(vbTab).Length > 1 Then
                            .CancelEdit()
                        End If

                        Dim dt As DataTable = Nothing

                        'datagridview could be unbound or bound to dataset, datatable, or bindingsource
                        'if need to add records to accomplish paste operation, must do to underlying datasource
                        If TypeOf .DataSource Is DataSet Then
                            dt = CType(.DataSource, DataSet).Tables(.DataMember)
                        ElseIf TypeOf .DataSource Is DataTable Then
                            dt = CType(.DataSource, DataTable)
                        ElseIf TypeOf .DataSource Is BindingSource Then
                            Dim bs As BindingSource = .DataSource
                            If TypeOf bs.DataSource Is DataSet Then
                                Dim DS As DataSet = bs.DataSource
                                Dim TableName As String = bs.DataMember
                                dt = DS.Tables(TableName)
                            ElseIf TypeOf bs.DataSource Is DataTable Then
                                dt = bs.DataSource
                            ElseIf TypeOf bs.DataSource.datasource Is DataSet Then
                                Dim DS As DataSet = CType(.DataSource, BindingSource).DataSource.datasource
                                Dim TableName As String = bs.DataSource.DataMember
                                dt = DS.Tables(TableName)
                            ElseIf TypeOf bs.DataSource.datasource.datasource Is DataSet Then
                                Dim DS As DataSet = bs.DataSource.datasource.datasource
                                Dim TableName As String = bs.DataSource.datasource.DataMember
                                dt = DS.Tables(TableName)
                            ElseIf TypeOf bs.DataSource.datasource.datasource.datasource Is DataSet Then
                                Dim DS As DataSet = bs.DataSource.datasource.datasource.datasource
                                Dim TableName As String = bs.DataSource.datasource.datasource.DataMember
                                dt = DS.Tables(TableName)
                            End If
                        Else
                            dt = Nothing
                        End If

                        For r As Integer = 0 To Rows.Length - 1
                            Dim Cols() As String = Rows(r).Split(vbTab)
                            Dim rr As Integer = r + minrow
                            Dim cc As Integer = mincol
                            Dim dr As DataRow = Nothing
                            Dim drv As DataRowView = Nothing

                            If dt Is Nothing Then 'is not bound; increase number of rows
                                If rr > dgv.Rows.Count - 1 AndAlso Rows(r).Trim <> "" Then dgv.Rows.Add()
                                'Else
                                'If rr > dt.Rows.Count - 1 And Rows(r).Trim <> "" Then dr = dt.NewRow : dt.Rows.Add(dr)
                            End If

                            If rr <= .RowCount - 1 Then
                                If dt IsNot Nothing Then dr = dt.Rows(rr)
                            Else
                                If Not .ReadOnly Then
                                    If dt Is Nothing Then .RowCount += 1 Else dr = dt.NewRow
                                Else
                                    Exit For
                                End If
                            End If

                            For c As Integer = 0 To Cols.Length - 1
                                If cc <= .Columns.Count - 1 AndAlso (rr > .RowCount - 1 OrElse Not .Item(cc, rr).ReadOnly) Then
                                    Dim v As Object = Cols(c)
                                    If Cols(c) = "" Then v = DBNull.Value
                                    If dt Is Nothing OrElse rr < .RowCount - 1 OrElse Not .Columns(cc).IsDataBound Then
                                        AddUndo(cc, rr, enumUndoAction.ChangeCell, UndoID)
                                        If IsNumeric(v) Then
                                            .Item(cc, rr).Value = Double.Parse(v) 'this will allow strings like 1,234.56 to be pasted properly
                                        Else
                                            .Item(cc, rr).Value = v
                                        End If
                                    Else
                                        'when you add unbound columns, they are inserted as column 0; therefore reference by name
                                        'dr.Item(cc) = v
                                        dr.Item(.Columns(cc).Name) = v
                                    End If
                                End If
                                'only paste to visible columns
                                Do
                                    cc += 1
                                Loop Until cc >= .ColumnCount - 1 OrElse .Columns(cc).Visible
                            Next

                            If dt IsNot Nothing Then
                                Try
                                    dt.Rows.Add(dr)
                                Catch ex As Exception
                                End Try
                            End If
                        Next

                        .AutoSizeColumnsMode = SavedAutoSizeColumnsMode
                        cm.RowCount = Rows.Length

                    Else 'only single cell in clipboard and multiple cells selected;repeat from source until all destination cells filled
                        For r As Integer = 0 To maxrow - minrow
                            Dim Cols() As String = Rows(r Mod Rows.Length).Split(vbTab)
                            Dim cc As Integer = mincol
                            For c As Integer = 0 To maxcol - mincol
                                Dim rr As Integer = minrow + r
                                If rr <= .RowCount - 1 AndAlso cc <= .ColumnCount - 1 AndAlso Not .Item(cc, rr).ReadOnly Then
                                    AddUndo(cc, rr, enumUndoAction.ChangeCell, UndoID)
                                    .Item(cc, rr).Value = Cols(c Mod Cols.Length)
                                End If
                                'only paste to visible columns
                                Do
                                    cc += 1
                                Loop Until cc >= .ColumnCount - 1 OrElse .Columns(cc).Visible
                            Next
                        Next

                        cm.RowCount = maxrow - minrow + 1

                    End If

                    dgv.ResumeLayout(True)
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "Delete" Then
                    Application.UseWaitCursor = True
                    For r As Integer = minrow To maxrow
                        For c As Integer = mincol To maxcol
                            If Not .Item(c, r).ReadOnly Then
                                AddUndo(c, r, enumUndoAction.ChangeCell, UndoID)
                                'If .Columns(c).ValueType Is GetType(Double) Or .Columns(c).ValueType Is GetType(Single) Or .Columns(c).ValueType Is GetType(Integer) Then
                                '    .Item(c, r).Value = 0
                                'Else
                                '    .Item(c, r).Value = DBNull.Value
                                'End If
                                .Item(c, r).Value = DBNull.Value
                            End If
                        Next
                    Next
                    RaiseEvent AfterContextMenu(Me, New ContextMenuEventArgs(ContextMenuEventArgs.ContextMenuTypeEnum.Delete))
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "SelectAll" Then
                    Application.UseWaitCursor = True
                    .SelectAll()

                ElseIf name = "Undo" Then
                    Undo()

                ElseIf name Like "Find*" Then 'find and find next
                    Dim r, c As Integer
                    Dim SearchInSelected As Boolean = .SelectedCells.Count > 1
                    If name = "Find" Then
                        sFind = InputBox(String.Format("Enter the string to find {0}. The case will be ignored. You may enter wildcard characters to broaden the search (for example, '*Test*' will match 'MyTEST' and 'Testing'):", IIf(SearchInSelected, "within the selected cells", "anywhere in the grid")), "Find", sFind)
                        If sFind = "" Then Exit Sub
                    Else 'is Find Next
                        r = .CurrentCell.RowIndex + 1
                        c = .CurrentCell.ColumnIndex
                        If r > .RowCount - 1 Then
                            r = 0
                            c += 1
                            If c > .ColumnCount - 1 Then Exit Sub
                        End If
                    End If
                    Application.UseWaitCursor = True
                    For cc As Integer = c To .ColumnCount - 1
                        For rr As Integer = r To .RowCount - 1
                            If .Item(cc, rr).Value IsNot Nothing AndAlso .Item(cc, rr).Value.ToString.ToUpper Like sFind.ToUpper Then
                                .ClearSelection()
                                .CurrentCell = .Item(cc, rr)
                                .CurrentCell.Selected = True
                                .FirstDisplayedCell = .CurrentCell
                                Exit Sub
                            End If
                        Next
                    Next
                    MessageBox.Show("No more occurrences of '" & sFind & "' were found.", "Find", MessageBoxButtons.OK, MessageBoxIcon.Information)

                ElseIf name = "Replace" Then
                    Dim r1, c1 As Int32
                    Dim SearchInSelected As Boolean = .SelectedCells.Count > 1
                    sFind = InputBox(String.Format("Enter the exact string to search for {0}. The case will NOT be ignored (for example, if you enter 'abc' as the search string and 'XYZ' as the replace string, a cell like 'Testabc' will be changed to 'TestXYZ'):", IIf(SearchInSelected, "within the selected cells", "anywhere in the grid")), "Search", sFind.Replace("*", ""))
                    If sFind = "" Then Exit Sub
                    Dim sReplace = InputBox(String.Format("Now enter the string to replace it with. Note that ALL occurrences of '{0}' throughout the entire grid will be replaced without confirmation. Use the special character ~ to indicate that you want to replace the string with nothing.", sFind), "Replace", sFind)
                    If sReplace = "" Then Exit Sub
                    If sReplace = "~" Then sReplace = ""
                    r1 = 0
                    c1 = 0
                    Dim NumReplaced As Int32 = 0
                    Application.UseWaitCursor = True
                    If SearchInSelected Then
                        For Each cell As DataGridViewCell In .SelectedCells
                            If Not cell.ReadOnly AndAlso cell.Value IsNot Nothing AndAlso cell.Value.ToString Like "*" & sFind & "*" Then
                                AddUndo(cell.ColumnIndex, cell.RowIndex, enumUndoAction.ChangeCell, UndoID)
                                cell.Value = cell.Value.ToString.Replace(sFind, sReplace)
                                NumReplaced += 1
                            End If
                        Next
                    Else
                        For r As Integer = r1 To .RowCount - 1
                            For c As Integer = c1 To .ColumnCount - 1
                                If Not .Item(c, r).ReadOnly AndAlso .Item(c, r).Value IsNot Nothing AndAlso .Item(c, r).Value.ToString Like "*" & sFind & "*" Then
                                    .ClearSelection()
                                    .Item(c, r).Selected = True
                                    AddUndo(c, r, enumUndoAction.ChangeCell, UndoID)
                                    .Item(c, r).Value = .Item(c, r).Value.ToString.Replace(sFind, sReplace)
                                    NumReplaced += 1
                                End If
                            Next
                        Next
                    End If
                    MessageBox.Show("A total of " & NumReplaced & " replacements were made.", "Search & Replace", MessageBoxButtons.OK, MessageBoxIcon.Information)

                ElseIf name Like "Sort*" Then
                    Application.UseWaitCursor = True
                    If Not .Columns(.CurrentCell.ColumnIndex).ReadOnly Then
                        .Sort(.Columns(.CurrentCell.ColumnIndex), System.ComponentModel.ListSortDirection.Ascending)
                        m_IsModified = True
                        RaiseEvent HasBeenModified(Me, New System.EventArgs)
                    End If

                ElseIf name = "Add" Then
                    s = InputBox("Enter the number to add to each selected cell (enter negative value to subtract):", "Add To Selected Values", "0")
                    If s = "" Then Exit Sub
                    Application.UseWaitCursor = True
                    For Each c As DataGridViewCell In .SelectedCells
                        If Not c.ReadOnly AndAlso IsNumeric(c.Value) Then
                            AddUndo(c.ColumnIndex, c.RowIndex, enumUndoAction.ChangeCell, UndoID)
                            c.Value += Math.Round(Val(s), 4)
                        End If
                    Next
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "Multiply" Then
                    s = InputBox("Enter the number to multiply by each selected cell:", "Multiply By Selected Values", "1")
                    If s = "" Then Exit Sub
                    Application.UseWaitCursor = True
                    For Each c As DataGridViewCell In .SelectedCells
                        If Not c.ReadOnly AndAlso IsNumeric(c.Value) Then
                            AddUndo(c.ColumnIndex, c.RowIndex, enumUndoAction.ChangeCell, UndoID)
                            c.Value *= Math.Round(Val(s), 4)
                        End If
                    Next
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "Series" Then
                    Dim v As Double
                    'If IsDate(.CurrentCell.Value) And MinColSel() = MaxColSel() Then
                    If IsDate(FirstCellValue) AndAlso mincol = maxcol Then
                        s = InputBox("Enter the number of hours between values in the series:", "Create Series", "1")
                        If s = "" Then Exit Sub
                        If Not IsNumeric(s) Then
                            WarningMsg("You must enter a valid number of hours.")
                            Exit Sub
                        End If
                        v = Val(s)
                        Dim c As Integer = mincol
                        'Dim startdate As Date = CDate(.CurrentCell.Value)
                        Dim startdate As Date = CDate(FirstCellValue())
                        If minrow = maxrow Then
                            s = InputBox("Because only a single cell was selected, you can now specify the maximum date you want and the appropriate number of rows below will be filled:", "Create Series", startdate.AddMonths(1))
                            If s = "" Then Exit Sub
                            If Not (IsDate(s) AndAlso CDate(s) > startdate) Then WarningMsg("You must enter a valid date that is larger that the starting date.") : Exit Sub
                            maxrow = minrow + CDate(s).Subtract(startdate).TotalHours / v
                            If maxrow > dgv.RowCount - 1 Then dgv.RowCount = maxrow + 1
                        End If
                        Application.UseWaitCursor = True
                        Application.DoEvents()
                        .Item(c, maxrow).Value = DBNull.Value 'so virtual grids have opportunity to redimension underlying data a single time
                        For r As Integer = minrow + 1 To maxrow
                            AddUndo(c, r, enumUndoAction.ChangeCell, UndoID)
                            .Item(c, r).Value = DateAdd(DateInterval.Minute, v * (r - minrow) * 60, startdate).ToString(MyDateTimeFormat)
                        Next
                        m_IsModified = True
                        RaiseEvent HasBeenModified(Me, New System.EventArgs)
                        'ElseIf IsNumeric(.CurrentCell.Value) And MinColSel() = MaxColSel() Then
                    ElseIf IsNumeric(FirstCellValue) AndAlso mincol = maxcol Then
                        s = InputBox("Enter the number to increment values in the series:", "Create Series", "1")
                        If s = "" Then Exit Sub
                        If Not IsNumeric(s) Then
                            WarningMsg("You must enter a valid number.")
                            Exit Sub
                        End If
                        v = Val(s)
                        Dim c As Integer = mincol
                        'Dim startval As Double = Val(.CurrentCell.Value)
                        Dim startval As Double = Val(FirstCellValue)
                        If minrow = maxrow Then
                            s = InputBox("Because only a single cell was selected, you can now specify the maximum value you want and the appropriate number of rows below will be filled:", "Create Series", startval + 100)
                            If s = "" Then Exit Sub
                            If Not (IsNumeric(s) AndAlso Val(s) > startval) Then WarningMsg("You must enter a valid number that is larger that the starting value.") : Exit Sub
                            maxrow = minrow + (Val(s) - startval) / v
                            If maxrow > dgv.RowCount - 1 AndAlso Not dgv.DataSource Is Nothing Then dgv.RowCount = maxrow + 1
                        End If
                        Application.UseWaitCursor = True
                        Application.DoEvents()
                        .Item(c, maxrow).Value = DBNull.Value 'so virtual grids have opportunity to redimension underlying data a single time
                        For r As Integer = minrow + 1 To maxrow
                            AddUndo(c, r, enumUndoAction.ChangeCell, UndoID)
                            .Item(c, r).Value = Math.Round(.Item(c, minrow).Value + v * (r - minrow), 4)
                        Next
                        m_IsModified = True
                        RaiseEvent HasBeenModified(Me, New System.EventArgs)
                    Else
                        WarningMsg("You must select either a single cell or a range of cells in a single column beginning with a valid date or number before a series can be created.")
                        Exit Sub
                    End If

                ElseIf name = "Append" Then
                    Dim NumAppend As Int32 = Val(InputBox("Enter the number of rows to append to the bottom of the grid:", "Append Rows", "10"))
                    If NumAppend = 0 Then Exit Sub
                    Application.UseWaitCursor = True
                    cm.RowIndex = .RowCount
                    cm.RowCount = NumAppend
                    .RowCount += NumAppend
                    .FirstDisplayedCell = .Item(1, .RowCount - NumAppend)
                    For r As Integer = .RowCount - NumAppend To .RowCount - 1
                        AddUndo(0, r, enumUndoAction.InsertRow, UndoID)
                        For c As Integer = 0 To .ColumnCount - 1
                            .Item(c, r).Selected = True
                        Next
                    Next
                    RenumberRows()
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "Insert" Then

                    Application.UseWaitCursor = True
                    If .VirtualMode Then
                        'caller must adjust the data appropriately
                        .RowCount += maxrow - minrow + 1
                    Else
                        '.Rows.Insert(.CurrentRow.Index, MaxRow - MinRow + 1)
                        .Rows.Insert(minrow, maxrow - minrow + 1)
                    End If
                    If .RowCount > 0 Then
                        For c As Integer = 0 To .ColumnCount - 1
                            If .Columns(c).Visible Then
                                .CurrentCell = .Item(c, minrow)
                                Exit For
                            End If
                        Next
                    End If
                    For r As Integer = minrow To maxrow
                        AddUndo(0, r, enumUndoAction.InsertRow, UndoID)
                        For c As Integer = 0 To .ColumnCount - 1
                            .Item(c, r).Selected = True
                        Next
                    Next
                    RenumberRows()
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "DuplicateRow" Then
                    'Application.UseWaitCursor = True
                    'If .VirtualMode Then
                    '    'caller must adjust the data appropriately
                    '    .RowCount += 1
                    'Else
                    '    If .DataSource Is Nothing Then
                    '        .Rows.Insert(minrow + 1, maxrow - minrow + 1)
                    '    Else
                    '        .Rows(minrow).Selected = True

                    '    End If
                    'End If
                    '.CurrentCell = .Item(0, minrow + 1)
                    'For r As Integer = minrow To maxrow
                    '    AddUndo(0, r, enumUndoAction.InsertRow, UndoID)
                    '    For c As Integer = 0 To .ColumnCount - 1
                    '        'need to finish this!
                    '    Next
                    'Next
                    'RenumberRows()
                    'm_IsModified = True
                    'RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "DeleteRows" Then

                    Dim SavedAutoSizeColumnsMode As DataGridViewAutoSizeColumnMode = .AutoSizeColumnsMode
                    .AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None
                    Application.UseWaitCursor = True
                    Dim NumDelete As Integer = maxrow - minrow + 1
                    For r As Integer = minrow To Math.Min(maxrow, .Rows.Count - 1)
                        For c As Integer = 0 To .ColumnCount - 1
                            AddUndo(c, r, enumUndoAction.ChangeCell, UndoID)
                        Next
                    Next
                    For r As Integer = 1 To NumDelete
                        AddUndo(0, minrow, DGVEditor.enumUndoAction.DeleteRow, UndoID)
                    Next
                    For r As Integer = 1 To NumDelete
                        If .VirtualMode Then
                            .RowCount -= 1
                        Else
                            If .DataSource Is Nothing Then
                                .Rows.RemoveAt(minrow)
                            Else
                                If TypeOf .DataSource Is BindingSource Then
                                    With CType(.DataSource, BindingSource)
                                        '.RemoveAt(minrow)
                                        .Remove(dgv.Rows(minrow).DataBoundItem)
                                    End With
                                ElseIf TypeOf .DataSource Is DataSet Then
                                    CType(.DataSource, DataSet).Tables(.DataMember).Rows.RemoveAt(minrow)
                                Else
                                    'CType(.DataSource, DataTable).Rows.RemoveAt(minrow)
                                    .Rows.RemoveAt(minrow)
                                End If
                            End If
                        End If
                        Dim pe As New ProgressEventArgs(r * 100.0 / NumDelete, True)
                        RaiseEvent ProgressChanged(Me, pe)
                        If pe.Cancel Then Exit For
                    Next
                    RaiseEvent ProgressComplete(Me)
                    'find first visible column
                    If .RowCount > 0 Then
                        For c As Integer = 0 To .ColumnCount - 1
                            If .Columns(c).Visible Then
                                .CurrentCell = .Item(c, Math.Min(minrow, .Rows.Count - 1))
                                Exit For
                            End If
                        Next
                    End If
                    RenumberRows()
                    .AutoSizeColumnsMode = SavedAutoSizeColumnsMode
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "ClearTable" Then
                    Application.UseWaitCursor = True
                    Dim NumDelete As Integer = dgv.Rows.Count
                    Dim SavedAutoSizeColumnsMode As DataGridViewAutoSizeColumnMode = .AutoSizeColumnsMode
                    .AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None
                    If True OrElse .DataSource Is Nothing Then
                        '.SelectAll()
                        Application.DoEvents()
                        dgv.SuspendLayout()
                        For r As Integer = 1 To NumDelete
                            For c As Integer = 0 To .ColumnCount - 1
                                AddUndo(c, 0, enumUndoAction.ChangeCell, UndoID)
                            Next
                            AddUndo(0, 0, enumUndoAction.DeleteRow, UndoID)
                            .Rows.RemoveAt(0)
                            Dim pe As New ProgressEventArgs(r * 100.0 / NumDelete, True)
                            RaiseEvent ProgressChanged(Me, pe)
                            If pe.Cancel Then Exit For
                        Next
                        RaiseEvent ProgressComplete(Me)
                        dgv.ResumeLayout(True)
                    Else
                        If TypeOf .DataSource Is BindingSource Then
                            If TypeOf (CType(.DataSource, BindingSource).DataSource) Is DataSet Then
                                Dim dt As DataTable = CType(CType(.DataSource, BindingSource).DataSource, DataSet).Tables(CType(.DataSource, BindingSource).DataMember)
                                'following is not quite right...but won't use anyway
                                'cannot use clear, because doesn't get committed
                                For i As Integer = 0 To dt.Rows.Count - 1
                                    dt.Rows(i).Delete()
                                Next
                            ElseIf TypeOf (CType(.DataSource, BindingSource).DataSource) Is DataTable Then
                                CType(CType(.DataSource, BindingSource).DataSource, DataTable).Rows.Clear()
                            Else
                                For r As Integer = 1 To NumDelete
                                    CType(.DataSource, BindingSource).RemoveAt(0)
                                Next
                            End If
                        ElseIf TypeOf .DataSource Is DataSet Then
                            CType(.DataSource, DataSet).Tables(.DataMember).Rows.Clear()
                        ElseIf TypeOf .DataSource Is DataTable Then
                            CType(.DataSource, DataTable).Rows.Clear()
                        End If

                        'following is too slow!
                        'For r As Integer = 1 To NumDelete
                        '    If TypeOf (.DataSource) Is BindingSource Then
                        '        CType(.DataSource, BindingSource).RemoveAt(0)
                        '    ElseIf TypeOf .DataSource Is DataSet Then
                        '        CType(.DataSource, DataSet).Tables(.DataMember).Rows.RemoveAt(0)
                        '    Else
                        '        CType(.DataSource, DataTable).Rows.RemoveAt(0)
                        '    End If
                        'Next
                    End If
                    RenumberRows()
                    .AutoSizeColumnsMode = SavedAutoSizeColumnsMode
                    m_IsModified = True
                    RaiseEvent HasBeenModified(Me, New System.EventArgs)

                ElseIf name = "Print" OrElse name = "PDF" Then
                    Dim dgvp As New DGVPrinter(dgv, printdoc)
                    With dgvp
                        .ProportionalColumns = (ReportScaling <> enumReportScaling.None)
                        .AllowWordWrap = (ReportScaling = enumReportScaling.Multiline)
                        .AutoReduceFont = (ReportScaling = enumReportScaling.ScaledFonts)
                        .CellFormatFlags = 0
                        .PageNumberFont = New Drawing.Font(.PageNumberFont, Drawing.FontStyle.Italic)
                        .PageNumberAlignment = Drawing.StringAlignment.Near
                        .PageNumberInHeader = False
                        .FooterAlignment = Drawing.StringAlignment.Far
                        .FooterFont = New Drawing.Font(.PageNumberFont, Drawing.FontStyle.Italic)
                        .Footer = Now.ToString("MMMM d, yyyy")
                        Application.UseWaitCursor = True
                        If name = "PDF" Then
                            .PrintDataGridViewToPDF(True)
                        Else
                            .PrintDataGridView(True)
                        End If
                    End With
                    dgvp = Nothing

                ElseIf name = "Excel" Then
                    SendToExcel()

                ElseIf name = "Stats" Then
                    If mincol <> maxcol Then
                        WarningMsg("You can only compute statistics for a single column.")
                        Exit Sub
                    End If
                    Dim lstData As New Generic.List(Of Double)

                    For r As Integer = 0 To dgv.RowCount - 1
                        If Not IsDBNull(.Item(mincol, r).Value) AndAlso IsNumeric(.Item(mincol, r).Value) Then
                            lstData.Add(CDbl(.Item(mincol, r).Value))
                        End If
                    Next
                    If lstData.Count = 0 Then
                        WarningMsg("No numeric value were found in the current column from which to compute statistics.")
                        Exit Sub
                    End If
                    Dim Sum, Min, Max, Mean, StdDev As Double, Count As Integer
                    CalcStats(lstData.ToArray, Sum, Mean, StdDev, Count, Min, Max)
                    MessageBox.Show(StringFormat("Count:\t{0}\nSum:\t{1:0.000}\nMean:\t{2:0.000}\nStd Dev:\t{3:0.000}\nMin:\t{4:0.000}\nMax:\t{5:0.000}", Count, Sum, Mean, StdDev, Min, Max), "Statistics", MessageBoxButtons.OK, MessageBoxIcon.Information)
                Else
                    'Debug.Fail("An invalid name has been passed to ItemClicked:" & name)
                End If
            End With

            RaiseEvent AfterContextMenu(Me, cm)
            cm = Nothing

        Catch ex As Exception
            ErrorMsg(, ex)
        Finally
            Application.UseWaitCursor = False
            dgv.AllowUserToAddRows = AllowAdd
        End Try
    End Sub

    Private Sub mnu_Opening(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles mnu.Opening
        'see if grid is read-only; if so, limit some of the menu items
        mnu.Items("Add").Visible = Not dgv.ReadOnly
        mnu.Items("Append").Visible = Not dgv.ReadOnly AndAlso dgv.DataSource Is Nothing AndAlso m_AllowRowChange
        mnu.Items("Cut").Visible = Not dgv.ReadOnly
        mnu.Items("Delete").Visible = Not dgv.ReadOnly
        mnu.Items("DeleteRows").Visible = Not dgv.ReadOnly AndAlso m_AllowRowChange
        mnu.Items("DuplicateRow").Visible = Not dgv.ReadOnly AndAlso m_AllowRowChange AndAlso dgv.SelectedRows.Count = 1 AndAlso False 'this is not programmed yet!
        mnu.Items("ClearTable").Visible = Not dgv.ReadOnly AndAlso m_AllowRowChange
        mnu.Items("Fill").Visible = Not dgv.ReadOnly
        mnu.Items("Insert").Visible = Not dgv.ReadOnly AndAlso dgv.DataSource Is Nothing AndAlso m_AllowRowChange
        mnu.Items("Multiply").Visible = Not dgv.ReadOnly
        mnu.Items("Paste").Visible = Not dgv.ReadOnly
        mnu.Items("Replace").Visible = Not dgv.ReadOnly
        mnu.Items("Paste").Enabled = Clipboard.ContainsText
        'mnu.Items("Paste").Enabled = dgv.DataSource IsNot Nothing
        'mnu.Items("Replace").Enabled = dgv.DataSource IsNot Nothing
        mnu.Items("Sort").Visible = Not (dgv.ReadOnly Or dgv.VirtualMode)
        mnu.Items("Series").Visible = Not dgv.ReadOnly
        For Each ts As ToolStripItem In mnu.Items
            If TypeOf ts Is ToolStripSeparator Then ts.Visible = Not dgv.ReadOnly
        Next
    End Sub

    Private Sub ProgressChangedHandler(ByVal sender As Object, ByVal Percent As Integer)
        RaiseEvent ProgressChanged(Me, New ProgressEventArgs(Percent, False))
        'bwExcel.ReportProgress(Percent)
    End Sub

    ''' <summary>
    ''' Place row numbers in the first column
    ''' </summary>
    Public Sub RenumberRows()
        Try
            With dgv
                If Not m_AutoNumberRows OrElse dgv.RowCount = 0 OrElse Not dgv.RowHeadersVisible Then Exit Sub
                '.SuspendLayout()
                'make first col narrow & centered for a row number
                .RowHeadersVisible = True
                .RowHeadersDefaultCellStyle.Font = New System.Drawing.Font(.DefaultCellStyle.Font, Drawing.FontStyle.Regular)
                .RowHeadersDefaultCellStyle.Alignment = Windows.Forms.DataGridViewContentAlignment.MiddleCenter
                .RowHeadersDefaultCellStyle.WrapMode = Windows.Forms.DataGridViewTriState.True
                .ShowRowErrors = True
                '.RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight
                .RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing

                'set row header width based on the largest possible row number
                .Rows(0).HeaderCell.Value = CStr(.RowCount)
                .AutoResizeRowHeadersWidth(DataGridViewRowHeadersWidthSizeMode.AutoSizeToDisplayedHeaders)
                .RowHeadersWidth *= 1.2 'so there is room for the glyph too

                If .VirtualMode Then
                    UpdateRowHeaders()
                Else
                    For i As Integer = 0 To .RowCount - 1
                        .Rows(i).HeaderCell.Value = CStr(i + 1)
                    Next
                End If
                '.ResumeLayout()
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Public Sub SendToExcel()
        Application.UseWaitCursor = True
        'bwExcel = New System.ComponentModel.BackgroundWorker
        'bwExcel.WorkerReportsProgress = True
        'bwExcel.WorkerSupportsCancellation = True
        'bwExcel.RunWorkerAsync()
        bwExcel_DoWork(Nothing, Nothing)
        Application.UseWaitCursor = False
    End Sub

    ''' <summary>
    ''' Replace specified cell with a link-type cell
    ''' </summary>
    ''' <param name="col">Desired column index</param>
    ''' <param name="row">Desired row index</param>
    Public Sub SetCellFormatLink(ByVal col As Integer, ByVal row As Integer)
        Try
            Dim dgvrow As DataGridViewRow = dgv.Rows(row).Clone
            For c As Integer = 0 To dgvrow.Cells.Count - 1
                dgvrow.Cells(c).Value = dgv.Rows(row).Cells(c).Value
            Next
            dgvrow.Cells.RemoveAt(col)
            Dim dgvLink As New DataGridViewLinkCell
            With dgvLink
                .Value = dgv.Rows(row).Cells(col).Value
                .UseColumnTextForLinkValue = True
                .TrackVisitedState = False
                .LinkColor = Drawing.Color.Blue
            End With
            dgvrow.Cells.Insert(col, dgvLink)
            dgvrow.Cells(col).ReadOnly = True
            dgv.Rows.RemoveAt(row)
            dgv.Rows.Insert(row, dgvrow)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' Replace all specified cells in a row with a combobox type cell (this give much better performance when dealing with rows having many comboboxes)
    ''' Note: values for cells must be set after calling this
    ''' </summary>
    ''' <param name="row">Desired row index</param>
    ''' <param name="ComboItems">Semicolon separated list of items for the combobox</param>
    Public Sub SetCellFormatList(ByVal row As Integer, ParamArray ComboItems() As String)
        Try
            With dgv
                If .CurrentCell Is Nothing Then Exit Sub
                Dim LastRow As Integer = .CurrentCell.RowIndex
                Dim LastCol As Integer = .CurrentCell.ColumnIndex

                Dim dgvrow As DataGridViewRow = .Rows(row).Clone
                For col As Integer = 0 To dgvrow.Cells.Count - 1
                    If col > ComboItems.Length - 1 OrElse String.IsNullOrEmpty(ComboItems(col)) Then
                        dgvrow.Cells(col).Value = .Rows(row).Cells(col).Value
                    Else
                        Dim arComboItems() As String = ComboItems(col).Split(";")
                        dgvrow.Cells.RemoveAt(col)
                        Dim dgvCombo As New DataGridViewComboBoxCell
                        With dgvCombo
                            .Items.AddRange(arComboItems)
                            .AutoComplete = True
                            .FlatStyle = FlatStyle.System
                            .DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton
                            .MaxDropDownItems = 12
                        End With
                        dgvrow.Cells.Insert(col, dgvCombo)
                    End If
                Next
                .Rows.RemoveAt(row)
                .Rows.Insert(row, dgvrow)
                .CurrentCell = .Item(LastCol, LastRow)
                .EditMode = DataGridViewEditMode.EditOnEnter 'if any are combo boxes, want this edit mode (so can single-click on combo box)
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' Replace specified cell with a combobox type cell
    ''' </summary>
    ''' <param name="col">Desired column index</param>
    ''' <param name="row">Desired row index</param>
    ''' <param name="ComboItems">Semicolon separated list of items for the combobox</param>
    ''' <param name="ExtraItem">Additional list item to add (if not already in prior list)</param>
    Public Sub SetCellFormatList(ByVal col As Integer, ByVal row As Integer, ByVal ComboItems As String, Optional ByVal ExtraItem As String = "", Optional ByVal AllowEditing As Boolean = False)
        Dim arComboItems() As String = ComboItems.Split(";")
        SetCellFormatList(col, row, arComboItems, ExtraItem, AllowEditing)
    End Sub

    ''' <summary>
    ''' Replace specified cell with a combobox type cell
    ''' </summary>
    ''' <param name="col">Desired column index</param>
    ''' <param name="row">Desired row index</param>
    ''' <param name="ComboItems">List of items for the combobox</param>
    ''' <param name="ExtraItem">Additional list item to add (if not already in prior list)</param>
    Public Sub SetCellFormatList(ByVal col As Integer, ByVal row As Integer, ByVal ComboItems As Generic.List(Of String), Optional ByVal ExtraItem As String = "", Optional ByVal AllowEditing As Boolean = False)
        Dim arComboItems() As String = ComboItems.ToArray
        SetCellFormatList(col, row, arComboItems, ExtraItem, AllowEditing)
    End Sub

    ''' <summary>
    ''' Replace specified cell with a combobox type cell (only works with unbound grids!)
    ''' </summary>
    ''' <param name="col">Desired column index</param>
    ''' <param name="row">Desired row index</param>
    ''' <param name="arComboItems">List of items for the combobox</param>
    ''' <param name="ExtraItem">Additional list item to add (if not already in prior list)</param>
    Private Sub SetCellFormatList(ByVal col As Integer, ByVal row As Integer, ByVal arComboItems() As String, Optional ByVal ExtraItem As String = "", Optional ByVal AllowEditing As Boolean = False)
        Try
            With dgv
                If .CurrentCell Is Nothing Then Exit Sub
                Dim LastRow As Integer = .CurrentCell.RowIndex
                Dim LastCol As Integer = .CurrentCell.ColumnIndex
                Dim LastItem As Object = .Item(col, row).Value

                Dim dgvrow As DataGridViewRow = .Rows(row).Clone
                For c As Integer = 0 To dgvrow.Cells.Count - 1
                    If c <> col Then dgvrow.Cells(c).Value = .Rows(row).Cells(c).Value
                Next
                dgvrow.Cells.RemoveAt(col)
                Dim dgvCombo As DataGridViewComboBoxCell
                If AllowEditing Then dgvCombo = New DataGridViewComboBoxEditCell(dgv) Else dgvCombo = New DataGridViewComboBoxCell
                With dgvCombo
                    .Items.AddRange(arComboItems)
                    If ExtraItem <> "" AndAlso Not .Items.Contains(ExtraItem) Then .Items.Add(ExtraItem)
                    .AutoComplete = True
                    .FlatStyle = FlatStyle.System
                    .DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton
                    .MaxDropDownItems = 12
                End With
                dgvrow.Cells.Insert(col, dgvCombo)
                .Rows.RemoveAt(row)
                .Rows.Insert(row, dgvrow)
                .CurrentCell = .Item(LastCol, LastRow)
                For Each s As String In arComboItems
                    If s = LastItem Then
                        .Item(col, row).Value = LastItem
                        Exit For
                    End If
                Next
                .EditMode = DataGridViewEditMode.EditOnEnter 'if any are combo boxes, want this edit mode (so can single-click on combo box)
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' Validate entry of values in this column
    ''' </summary>
    ''' <param name="Col">Column number</param>
    Public Sub SetCellValidation(ByVal Col As Integer, ByVal Row As Integer, ByVal ValidationType As Type, ByVal AllowNull As Boolean)
        Dim Validation As New clsValidation(ValidationType, AllowNull)
        dgv.Item(Col, Row).Tag = Validation
    End Sub

    ''' <summary>
    ''' Validate entry of values in this column
    ''' </summary>
    ''' <param name="Col">Column number</param>
    Public Sub SetCellValidation(ByVal Col As Integer, ByVal Row As Integer, ByVal ValidationType As Type, ByVal AllowNull As Boolean, ByVal MinValue As Object, ByVal MaxValue As Object)
        Dim Validation As New clsValidation(ValidationType, AllowNull, MinValue, MaxValue)
        dgv.Item(Col, Row).Tag = Validation
    End Sub

    ''' <summary>
    ''' Replace specified column with a specified type column (will have to reload grid)
    ''' </summary>
    ''' <param name="ColumnType">Type of column to set</param>
    ''' <param name="col">Desired column index</param>
    ''' <param name="Flag">Optional Flag (used only for SortByName in ColorPicker)</param>
    Public Sub SetColumnFormat(ByVal ColumnType As enumColumnTypes, ByVal col As Integer, Optional Flag As Boolean = False)
        Try
            Dim dgvColOld As DataGridViewColumn = dgv.Columns(col)
            Dim dgvColNew As DataGridViewColumn = Nothing
            Select Case ColumnType
                Case enumColumnTypes.None : dgvColNew = New DataGridViewTextBoxColumn
                Case enumColumnTypes.ComboBox : dgvColNew = New DataGridViewComboBoxColumn
                Case enumColumnTypes.ComboBoxEdit : dgvColNew = New DataGridViewComboBoxEditColumn
                Case enumColumnTypes.CheckBox : dgvColNew = New DataGridViewCheckBoxColumn
                Case enumColumnTypes.Calendar : dgvColNew = New DataGridViewCalendarColumn
                Case enumColumnTypes.Link : dgvColNew = New DataGridViewLinkColumn
                Case enumColumnTypes.ColorPicker : dgvColNew = New DataGridViewColorPickerColumn
                Case enumColumnTypes.Button
                    dgvColNew = New DataGridViewButtonColumn
                    CType(dgvColNew, DataGridViewButtonColumn).UseColumnTextForButtonValue = True
                    'CType(dgvColNew, DataGridViewButtonColumn).Text = dgv.Columns(col).HeaderText
                    'dgv.Columns(col).HeaderText = ""
            End Select

            With dgvColNew
                .DataPropertyName = dgvColOld.DataPropertyName
                .HeaderText = dgvColOld.HeaderText
                .Name = dgvColOld.Name
                .AutoSizeMode = dgvColOld.AutoSizeMode
                .SortMode = dgvColOld.SortMode
            End With

            Select Case ColumnType
                Case enumColumnTypes.ComboBox, enumColumnTypes.ColorPicker
                    With CType(dgvColNew, DataGridViewComboBoxColumn)
                        .AutoComplete = True
                        .FlatStyle = FlatStyle.System
                        .DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton
                        .MaxDropDownItems = 12
                    End With
                    If ColumnType = enumColumnTypes.ColorPicker Then
                        With CType(dgvColNew, DataGridViewColorPickerColumn)
                            .SortByName = Flag
                        End With
                    End If
                Case enumColumnTypes.ComboBoxEdit
                Case enumColumnTypes.Button
                    With CType(dgvColNew, DataGridViewButtonColumn)
                        .Text = .HeaderText
                        .HeaderText = ""
                        .AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells
                    End With
                Case enumColumnTypes.Link
                    With CType(dgvColNew, DataGridViewLinkColumn)
                        .LinkColor = Color.Blue
                        .TrackVisitedState = False
                    End With
            End Select
            dgv.Columns.RemoveAt(col)
            dgv.Columns.Insert(col, dgvColNew)
            dgv.Columns(col).ValueType = Choose(ColumnType + 1, GetType(String), GetType(String), GetType(String), GetType(Boolean), GetType(Date), GetType(String), GetType(String), GetType(String))

            'note, above two stmts cause CellEndEdit event to fire, which sets modified flag (don't want that)--caller should reset modified flag to false
            'dgv.EditMode = DataGridViewEditMode.EditOnEnter 'if any are combo boxes, want this edit mode (so can single-click on combo box)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' Replace specified column with a specified type column (will have to reload grid)
    ''' </summary>
    ''' <param name="ColumnType">Type of column to set</param>
    ''' <param name="col">Desired column index</param>
    ''' <param name="ComboItems">List of items for the combobox (only if type is ComboBox)</param>
    Public Sub SetColumnFormat(ColumnType As enumColumnTypes, col As Integer, ComboItems As Generic.List(Of String))
        Try
            SetColumnFormat(ColumnType, col)

            Select Case ColumnType
                Case enumColumnTypes.ComboBox, enumColumnTypes.ColorPicker
                    With CType(dgv.Columns(col), DataGridViewComboBoxColumn)
                        If ComboItems IsNot Nothing Then
                            .Items.AddRange(ComboItems.ToArray)
                        End If
                    End With
                Case enumColumnTypes.ComboBoxEdit
                    For r As Integer = 0 To dgv.RowCount - 1
                        dgv.CurrentCell = dgv.Item(col, r)
                        SetCellFormatList(col, r, ComboItems.ToArray, , True)
                    Next
            End Select

            'note, above two stmts cause CellEndEdit event to fire, which sets modified flag (don't want that)--caller should reset modified flag to false
            'dgv.EditMode = DataGridViewEditMode.EditOnEnter 'if any are combo boxes, want this edit mode (so can single-click on combo box)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' Replace specified column with a specified type column (will have to reload grid)
    ''' </summary>
    ''' <param name="ColumnType">Type of column to set</param>
    ''' <param name="PropertyName">Name of that property (don't know how to automatically determine from object)</param>
    Public Sub SetColumnFormat(ColumnType As enumColumnTypes, PropertyName As String)
        Try
            Dim col As Integer = dgv.Columns(PropertyName).Index
            SetColumnFormat(ColumnType, col)
            If ColumnType = enumColumnTypes.ComboBox Then
                With CType(dgv.Columns(col), DataGridViewComboBoxColumn)
                    .DataSource = Nothing
                    .DataPropertyName = ""
                    'assume datasource is list of class, but must have at least one item
                    If dgv.DataSource.count > 0 Then .DataSource = [Enum].GetValues(CallByName(dgv.DataSource(0), PropertyName, CallType.Get).GetType)
                    .DataPropertyName = PropertyName
                End With
            End If
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ' ''' <summary>
    ' ''' Replace specified column with a specified type column (will have to reload grid)
    ' ''' </summary>
    ' ''' <param name="ColumnType">Type of column to set</param>
    ' ''' <param name="col">Desired column index</param>
    ' ''' <param name="ComboItems">List of items for the combobox (only if type is ComboBox)</param>
    'Public Sub SetColumnFormat(ByVal ColumnType As enumColumnTypes, ByVal col As Integer, Optional ByVal ComboItems As Generic.List(Of String) = Nothing)
    '    Try
    '        Dim dgvColOld As DataGridViewColumn = dgv.Columns(col)
    '        Dim dgvColNew As DataGridViewColumn = Nothing
    '        Select Case ColumnType
    '            Case enumColumnTypes.None : dgvColNew = New DataGridViewTextBoxColumn
    '            Case enumColumnTypes.ComboBox : dgvColNew = New DataGridViewComboBoxColumn
    '            Case enumColumnTypes.ComboBoxEdit : dgvColNew = New DataGridViewComboBoxEditColumn
    '            Case enumColumnTypes.CheckBox : dgvColNew = New DataGridViewCheckBoxColumn
    '            Case enumColumnTypes.Calendar : dgvColNew = New DataGridViewCalendarColumn
    '            Case enumColumnTypes.Link : dgvColNew = New DataGridViewLinkColumn
    '            Case enumColumnTypes.ColorPicker : dgvColNew = New DataGridViewColorPickerColumn
    '            Case enumColumnTypes.Button
    '                dgvColNew = New DataGridViewButtonColumn
    '                CType(dgvColNew, DataGridViewButtonColumn).UseColumnTextForButtonValue = True
    '                'CType(dgvColNew, DataGridViewButtonColumn).Text = dgv.Columns(col).HeaderText
    '                'dgv.Columns(col).HeaderText = ""
    '        End Select

    '        With dgvColNew
    '            .DataPropertyName = dgvColOld.DataPropertyName
    '            .HeaderText = dgvColOld.HeaderText
    '            .Name = dgvColOld.Name
    '            .AutoSizeMode = dgvColOld.AutoSizeMode
    '            .SortMode = dgvColOld.SortMode
    '        End With

    '        Select Case ColumnType
    '            Case enumColumnTypes.ComboBox, enumColumnTypes.ColorPicker
    '                With CType(dgvColNew, DataGridViewComboBoxColumn)
    '                    If ComboItems IsNot Nothing Then
    '                        For i As Integer = 0 To ComboItems.Count - 1
    '                            If Not String.IsNullOrEmpty(ComboItems(i)) Then .Items.Add(ComboItems(i))
    '                        Next
    '                    End If
    '                    .AutoComplete = True
    '                    .FlatStyle = FlatStyle.System
    '                    .DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton
    '                    .MaxDropDownItems = 12
    '                End With
    '            Case enumColumnTypes.ComboBoxEdit
    '                For r As Integer = 0 To dgv.RowCount - 1
    '                    dgv.CurrentCell = dgv.Item(col, r)
    '                    SetCellFormatList(col, r, ComboItems.ToArray, , True)
    '                Next
    '                'With CType(dgvColNew, DataGridViewComboBoxEditColumn)
    '                '    If ComboItems IsNot Nothing Then
    '                '        For i As Integer = 0 To ComboItems.Count - 1
    '                '            If Not String.IsNullOrEmpty(ComboItems(i)) Then .Items.Add(ComboItems(i))
    '                '        Next
    '                '    End If
    '                'End With
    '            Case enumColumnTypes.Button
    '                With CType(dgvColNew, DataGridViewButtonColumn)
    '                    .Text = .HeaderText
    '                    .HeaderText = ""
    '                    .AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells
    '                End With
    '            Case enumColumnTypes.Link
    '                With CType(dgvColNew, DataGridViewLinkColumn)
    '                    .LinkColor = Color.Blue
    '                    .TrackVisitedState = False
    '                End With
    '        End Select
    '        dgv.Columns.RemoveAt(col)
    '        dgv.Columns.Insert(col, dgvColNew)
    '        dgv.Columns(col).ValueType = Choose(ColumnType + 1, GetType(String), GetType(String), GetType(String), GetType(Boolean), GetType(Date), GetType(String), GetType(String), GetType(String))

    '        'note, above two stmts cause CellEndEdit event to fire, which sets modified flag (don't want that)--caller should reset modified flag to false
    '        'dgv.EditMode = DataGridViewEditMode.EditOnEnter 'if any are combo boxes, want this edit mode (so can single-click on combo box)
    '    Catch ex As Exception
    '        ErrorMsg(, ex)
    '    End Try
    'End Sub

    ''' <summary>
    ''' Validate entry of values in this column
    ''' </summary>
    ''' <param name="Col">Column number</param>
    Public Sub SetColumnValidation(ByVal Col As Integer, ByVal ValidationType As Type, ByVal AllowNull As Boolean)
        Dim Validation As New clsValidation(ValidationType, AllowNull)
        dgv.Columns(Col).Tag = Validation
    End Sub

    ''' <summary>
    ''' Validate entry of values in this column
    ''' </summary>
    ''' <param name="Col">Column number</param>
    ''' <param name="MinValue">Minimum allowable value</param>
    ''' <param name="MaxValue">Maximum allowable value</param>
    Public Sub SetColumnValidation(ByVal Col As Integer, ByVal ValidationType As Type, ByVal AllowNull As Boolean, ByVal MinValue As Object, ByVal MaxValue As Object)
        Dim Validation As New clsValidation(ValidationType, AllowNull, MinValue, MaxValue)
        dgv.Columns(Col).Tag = Validation
    End Sub

    ''' <summary>
    ''' Validate entry of values in this column
    ''' </summary>
    ''' <param name="Col">Column number</param>
    ''' <param name="MinValue">Minimum allowable value</param>
    ''' <param name="MaxValue">Maximum allowable value</param>
    Public Sub SetColumnValidation(ByVal Col As Integer, ByVal ValidationType As Type, ByVal AllowNull As Boolean, ByVal MinValue As Object, ByVal MaxValue As Object, ByVal MinRange As Object, ByVal MaxRange As Object)
        Dim Validation As New clsValidation(ValidationType, AllowNull, MinValue, MaxValue, MinRange, MaxRange)
        dgv.Columns(Col).Tag = Validation
    End Sub

    ''' <summary>
    ''' Validate entry of values in this column
    ''' </summary>
    ''' <param name="Col">Column number</param>
    ''' <param name="MinValue">Minimum allowable value</param>
    ''' <param name="MaxValue">Maximum allowable value</param>
    Public Sub SetColumnValidation(ByVal Col As Integer, ByVal Row As Integer, ByVal ValidationType As Type, ByVal AllowNull As Boolean, ByVal MinValue As Object, ByVal MaxValue As Object)
        Dim Validation As New clsValidation(ValidationType, AllowNull, MinValue, MaxValue)
        dgv.Item(Col, Row).Tag = Validation
    End Sub

    ''' <summary>
    ''' Validate entry of values in this column
    ''' </summary>
    ''' <param name="Col">Column number</param>
    ''' <param name="MinValue">Minimum allowable value</param>
    ''' <param name="MaxValue">Maximum allowable value</param>
    Public Sub SetColumnValidation(ByVal Col As Integer, ByVal Row As Integer, ByVal ValidationType As Type, ByVal AllowNull As Boolean, ByVal MinValue As Object, ByVal MaxValue As Object, ByVal MinRange As Object, ByVal MaxRange As Object)
        Dim Validation As New clsValidation(ValidationType, AllowNull, MinValue, MaxValue, MinRange, MaxRange)
        dgv.Item(Col, Row).Tag = Validation
    End Sub

    ''' <summary>
    ''' Set the indicated column width to the width of a string passed
    ''' </summary>
    ''' <param name="Col">Column number</param>
    ''' <param name="Text">String to use to determine column width</param>
    Public Sub SetColumnWidth(ByVal Col As Integer, ByVal Text As String)
        With dgv.Columns(Col)
            .Width = dgv.CreateGraphics.MeasureString(Text, dgv.DefaultCellStyle.Font, 1000).Width
        End With
    End Sub

    ''' <summary>
    ''' Set column headings (double height) and default formatting; all column widths are at default except dates (wider) and boolean (auto)
    ''' </summary>
    ''' <param name="DelimitedColumnHeadings">Use ~ to delimit column, | to force newline in heading</param>
    ''' <param name="AutoNumberRows">If true, row numbers will be added as row headings</param>
    ''' <param name="RowCount">Number of rows to initially add</param>
    ''' <param name="VirtualMode">If true, grid will use virtual mode (must handle CellValueNeeded and Pushed events)</param>
    Public Sub SetHeadings(ByVal DelimitedColumnHeadings As String, Optional ByVal AutoNumberRows As Boolean = True, Optional ByVal RowCount As Integer = 50, Optional ByVal VirtualMode As Boolean = False)
        Try
            'pass all column headings as a string
            'separate columns with the ~ character (number of columns will be inferred)
            'to have multiple lines in a column heading, include the | character for vblf
            'this and AddRow are nice for building reports

            m_AutoNumberRows = AutoNumberRows

            With dgv
                '.SuspendLayout()
                '.VirtualMode = VirtualMode
                .VirtualMode = False
                .SelectAll()
                .ClearSelection()
                .AllowUserToOrderColumns = False
                .EditMode = Windows.Forms.DataGridViewEditMode.EditOnKeystrokeOrF2
                .ShowCellErrors = True
                .StandardTab = False
                .ClipboardCopyMode = Windows.Forms.DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText
                Dim cells As String() = DelimitedColumnHeadings.Split("~")
                .SelectionMode = DataGridViewSelectionMode.CellSelect
                .Columns.Clear()

                'do the following to avoid errors when adding lots of columns
                .ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing
                Dim cols(cells.Length - 1) As DataGridViewColumn
                For i As Integer = 0 To cells.Length - 1
                    Dim col As New DataGridViewTextBoxColumn
                    col.FillWeight = 1
                    cols(i) = col
                Next
                .Columns.AddRange(cols)

                '...disabled the following in favor of above
                '.ColumnCount = cells.Length

                .RowHeadersVisible = True
                .Rows.Clear()
                .ColumnHeadersVisible = True
                'set style for column headings
                .ColumnHeadersDefaultCellStyle.Font = New System.Drawing.Font(.DefaultCellStyle.Font, Drawing.FontStyle.Bold)
                .ColumnHeadersDefaultCellStyle.WrapMode = Windows.Forms.DataGridViewTriState.True
                .ColumnHeadersDefaultCellStyle.Alignment = Windows.Forms.DataGridViewContentAlignment.MiddleCenter
                .AllowUserToOrderColumns = False
                .AllowUserToResizeColumns = False
                .AllowUserToResizeRows = False
                .EnableHeadersVisualStyles = False
                .RowTemplate.Resizable = DataGridViewTriState.False
                .RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing
                .RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter

                'set headings and default formats for columns
                For c As Integer = 0 To .ColumnCount - 1
                    With .Columns(c)
                        dgv.Columns(c).SortMode = DataGridViewColumnSortMode.NotSortable
                        .Name = cells(c)
                        .HeaderText = cells(c).Replace("|", vbLf)
                        .Resizable = DataGridViewTriState.False
                        If cells(c) Like "*Date*" Then
                            .ValueType = GetType(DateTime)
                            .DefaultCellStyle.Format = MyDateTimeFormat()
                            .DefaultCellStyle.Alignment = Windows.Forms.DataGridViewContentAlignment.MiddleLeft
                            'SetColumnWidth(c, "00/00/0000 00:00_______________")
                            .AutoSizeMode = DataGridViewAutoSizeColumnMode.DisplayedCells
                        ElseIf cells(c).EndsWith("?") Then
                            .ValueType = GetType(Boolean)
                            SetColumnFormat(enumColumnTypes.CheckBox, c)
                            dgv.AutoResizeColumn(c, DataGridViewAutoSizeColumnMode.ColumnHeader)
                        Else
                            .ValueType = GetType(Double)
                            .DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight
                        End If
                    End With
                Next
                .ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize
                .SelectionMode = DataGridViewSelectionMode.ColumnHeaderSelect
                .CausesValidation = False
                .RowCount = RowCount
                .VirtualMode = VirtualMode
                RenumberRows()

                .CausesValidation = True
                '.ResumeLayout()
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' Set data source and column headings (double height) and default formatting; all column widths are at default except dates (wider) and boolean (auto)
    ''' </summary>
    ''' <param name="DataSource">Object containing data for table</param>
    ''' <param name="AutoNumberRows">If true, row numbers will be added as row headings</param>
    Public Sub SetDataSource(DataSource As Object, Optional ByVal AutoNumberRows As Boolean = True)
        Try
            'pass all column headings as a string
            'separate columns with the ~ character (number of columns will be inferred)
            'to have multiple lines in a column heading, include the | character for vblf
            'this and AddRow are nice for building reports

            m_AutoNumberRows = AutoNumberRows

            With dgv
                .SuspendLayout()
                .DoubleBuffered(True)

                'must reset selectmode
                .SelectionMode = DataGridViewSelectionMode.CellSelect

                .Columns.Clear()

                .DataSource = Nothing
                .DataSource = DataSource
                .AllowUserToOrderColumns = False
                .EditMode = Windows.Forms.DataGridViewEditMode.EditOnKeystrokeOrF2
                .ShowCellErrors = True
                .StandardTab = False
                .ClipboardCopyMode = Windows.Forms.DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText
                .SelectionMode = DataGridViewSelectionMode.CellSelect

                'do the following to avoid errors when adding lots of columns
                .ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing
                For i As Integer = 0 To .ColumnCount - 1
                    .Columns(i).FillWeight = 1
                Next

                .RowHeadersVisible = True
                .ColumnHeadersVisible = True
                'set style for column headings
                .ColumnHeadersDefaultCellStyle.Font = New System.Drawing.Font(.DefaultCellStyle.Font, Drawing.FontStyle.Bold)
                .ColumnHeadersDefaultCellStyle.WrapMode = Windows.Forms.DataGridViewTriState.True
                .ColumnHeadersDefaultCellStyle.Alignment = Windows.Forms.DataGridViewContentAlignment.MiddleCenter
                .AllowUserToOrderColumns = False
                .AllowUserToResizeColumns = False
                .AllowUserToResizeRows = False
                .EnableHeadersVisualStyles = False
                .RowTemplate.Resizable = DataGridViewTriState.False
                .RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing
                .RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter

                Dim DefaultWidth As Integer = dgv.CreateGraphics.MeasureString("".PadRight(8, "W"), dgv.DefaultCellStyle.Font, 1000).Width

                'set headings and default formats for columns
                For c As Integer = 0 To .ColumnCount - 1
                    With .Columns(c)
                        dgv.Columns(c).SortMode = DataGridViewColumnSortMode.NotSortable
                        .Resizable = DataGridViewTriState.False
                        .Width = DefaultWidth
                        If .ValueType = GetType(Date) Then
                            .DefaultCellStyle.Format = MyDateTimeFormat()
                            .DefaultCellStyle.Alignment = Windows.Forms.DataGridViewContentAlignment.MiddleLeft
                            'SetColumnWidth(c, "00/00/0000 00:00_______________")
                            .AutoSizeMode = DataGridViewAutoSizeColumnMode.DisplayedCells
                        ElseIf .ValueType = GetType(Boolean) Then
                            .ValueType = GetType(Boolean)
                            SetColumnFormat(enumColumnTypes.CheckBox, c)
                            dgv.AutoResizeColumn(c, DataGridViewAutoSizeColumnMode.ColumnHeader)
                        ElseIf .ValueType.IsEnum Then
                            SetColumnFormat(enumColumnTypes.ComboBox, .DataPropertyName)
                            .Width = 100 'make it wider
                        Else
                            .DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight
                        End If
                    End With
                Next
                .ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize
                .SelectionMode = DataGridViewSelectionMode.ColumnHeaderSelect
                .CausesValidation = False
                RenumberRows()

                .CausesValidation = True
                .ResumeLayout(True)
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Public Sub Undo(Optional ByVal NumToUndo As Integer = 1)
        dgv.SuspendLayout()
        For i As Integer = 1 To NumToUndo
            If UndoList.Count = 0 Then Exit Sub
            Dim ID As Integer = UndoList.Peek.UndoID
            While UndoList.Count > 0 AndAlso UndoList.Peek.UndoID = ID
                Dim UndoItem As structUndo = UndoList.Pop
                With UndoItem
                    Select Case .Action
                        Case enumUndoAction.ChangeCell
                            dgv.Item(.Column, .Row).Value = .Value
                            dgv.Item(.Column, .Row).ErrorText = ""
                            dgv.CurrentCell = dgv.Item(.Column, .Row)
                        Case enumUndoAction.DeleteColumn
                            dgv.Columns.Insert(.Column, New DataGridViewColumn)
                        Case enumUndoAction.InsertColumn
                            dgv.Columns.RemoveAt(.Column)
                        Case enumUndoAction.DeleteRow
                            'must do following in case is in virtual mode and caller needs to deal with arrays/lists
                            Dim cm As New ContextMenuEventArgs
                            cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.InsertRows
                            cm.RowIndex = .Row
                            cm.RowCount = 1
                            RaiseEvent BeforeContextMenu(Me, cm)
                            If Not cm.Cancel Then
                                dgv.Rows.Insert(.Row, New DataGridViewRow)
                                RaiseEvent AfterContextMenu(Me, cm)
                                'If Not cm.Cancel Then
                                '    Undo() 'undo all individual change events (will all have same ID)
                                'End If
                            End If
                        Case enumUndoAction.InsertRow
                            'must do following in case is in virtual mode and caller needs to deal with arrays/lists
                            Dim cm As New ContextMenuEventArgs
                            cm.ContextMenuType = ContextMenuEventArgs.ContextMenuTypeEnum.DeleteRows
                            cm.RowIndex = .Row
                            cm.RowCount = 1
                            RaiseEvent BeforeContextMenu(Me, cm)
                            If Not cm.Cancel Then
                                dgv.Rows.RemoveAt(.Row)
                                RaiseEvent AfterContextMenu(Me, cm)
                            End If
                    End Select
                End With
            End While
        Next
        RenumberRows()
        dgv.ResumeLayout()
    End Sub

    ''' <summary>
    ''' If in virtual mode, need to trap some events to set the row headers
    ''' </summary>
    ''' <remarks>For some reason, alignment or placement of row headers with numbers less than 100 is different than for those over 100!!!!????!!!! Tried to find work-around but no luck</remarks>
    Private Sub UpdateRowHeaders()
        If m_AutoNumberRows AndAlso dgv.VirtualMode Then
            For r As Integer = dgv.FirstDisplayedScrollingRowIndex To dgv.FirstDisplayedScrollingRowIndex + dgv.DisplayedRowCount(True)
                If r < 0 OrElse r > dgv.RowCount - 1 Then Exit Sub
                dgv.Rows(r).HeaderCell.Value = CStr(r + 1)
            Next
        End If
    End Sub

    Private Sub ValidateCell(ByVal ColumnIndex As Integer, ByVal RowIndex As Integer, ByRef CellStyle As DataGridViewCellStyle, ByRef Value As Object)
        Try
            If ColumnIndex < 0 OrElse RowIndex < 0 Then Exit Sub
            With dgv.Item(ColumnIndex, RowIndex)
                If .ReadOnly Then Exit Sub
                Dim IsError As Boolean = False, IsWarning As Boolean = False
                If Not TypeOf .Tag Is clsValidation And Not TypeOf dgv.Columns(ColumnIndex).Tag Is clsValidation Then Exit Sub
                'cell validation takes precedence over column validation
                Dim Validation As clsValidation = .Tag
                If Validation Is Nothing Then Validation = dgv.Columns(ColumnIndex).Tag
                If Validation IsNot Nothing Then
                    CellStyle.BackColor = dgv.DefaultCellStyle.BackColor
                    .ToolTipText = ""
                    If Not Validation.AllowNull AndAlso Value Is Nothing Then CellStyle.BackColor = Color.Red : .ToolTipText = String.Format("You must enter a value.") : Exit Sub
                    If Validation.ValidationType Is GetType(Double) OrElse Validation.ValidationType Is GetType(Single) Then
                        If Not IsNumeric(Value) Then CellStyle.BackColor = Color.Red : .ToolTipText = "You must enter a valid number." : Exit Sub
                    ElseIf Validation.ValidationType Is GetType(Integer) Then
                        If Not (IsNumeric(Value) AndAlso Val(Value) = CInt(Val(Value))) Then CellStyle.BackColor = Color.Red : .ToolTipText = "You must enter a valid integer." : Exit Sub
                    ElseIf Validation.ValidationType Is GetType(DateTime) Then
                        If IsDBNull(Value) OrElse Not IsDate(Value) Then CellStyle.BackColor = Color.Red : .ToolTipText = "You must enter a valid date." : Exit Sub
                    End If
                    If (Validation.MinValue IsNot Nothing AndAlso Value < Validation.MinValue) OrElse (Validation.MaxValue IsNot Nothing AndAlso Value > Validation.MaxValue) Then
                        CellStyle.BackColor = Color.Red
                        .ToolTipText = String.Format("This is outside the allowable range of values: {0} to {1}.", TestNull(Validation.MinValue, "(N/A)"), TestNull(Validation.MaxValue, "(N/A)"))
                        Exit Sub
                    End If
                    If (Validation.MinRange IsNot Nothing AndAlso Value < Validation.MinRange) OrElse (Validation.MaxRange IsNot Nothing AndAlso Value > Validation.MaxRange) Then
                        CellStyle.BackColor = Color.Yellow
                        .ToolTipText = String.Format("This is outside the recommended range of values: {0} to {1}.", TestNull(Validation.MinRange, "(N/A)"), TestNull(Validation.MaxRange, "(N/A)"))
                        Exit Sub
                    End If
                End If
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    ''' <summary>
    ''' If true (the default), will allow inserting, appending, and deleting of entire rows
    ''' </summary>
    Public Property AllowRowChange() As Boolean
        Get
            Return m_AllowRowChange
        End Get
        Set(ByVal value As Boolean)
            m_AllowRowChange = value
        End Set
    End Property

    ''' <summary>
    ''' Force the grid to copy edits in current cell to all selected cells
    ''' </summary>
    Public Property AutoFillSelectedCells() As Boolean
        Get
            Return m_AutoFillSelectedCells
        End Get
        Set(ByVal value As Boolean)
            m_AutoFillSelectedCells = value
        End Set
    End Property

    ''' <summary>
    ''' If true (the default), will place the row number in the row header and automatically keep them updated
    ''' </summary>
    Public Property AutoNumberRows() As Boolean
        Get
            Return m_AutoNumberRows
        End Get
        Set(ByVal value As Boolean)
            m_AutoNumberRows = value
        End Set
    End Property
    ''' <summary>
    ''' Returns the datagridview referenced by the editor
    ''' </summary>
    Public ReadOnly Property DataGridView() As DataGridView
        Get
            Return dgv
        End Get
    End Property

    ''' <summary>
    ''' Returns True if data have been modified in the grid
    ''' </summary>
    Public Property IsModified() As Boolean
        Get
            Return m_IsModified
        End Get
        Set(ByVal value As Boolean)
            m_IsModified = value
        End Set
    End Property

    ''' <summary>
    ''' Get or set selected column as read-only and make it appear grayed-out if true
    ''' </summary>
    ''' <param name="Col">Column number</param>
    Public Property ReadOnlyColumn(ByVal Col As Integer) As Boolean
        Get
            Return dgv.Columns(Col).ReadOnly
        End Get
        Set(ByVal value As Boolean)
            With dgv.Columns(Col)
                .ReadOnly = value
                .DefaultCellStyle.ForeColor = IIf(value, Drawing.SystemColors.GrayText, Drawing.Color.Blue)
            End With
        End Set
    End Property

    ''' <summary>
    ''' Get or set selected row as read-only and make it appear grayed-out if true
    ''' </summary>
    ''' <param name="Row">Row number</param>
    Public Property ReadOnlyRow(ByVal Row As Integer) As Boolean
        Get
            Return dgv.Rows(Row).ReadOnly
        End Get
        Set(ByVal value As Boolean)
            With dgv.Rows(Row)
                .ReadOnly = value
                .DefaultCellStyle.ForeColor = IIf(value, Drawing.SystemColors.GrayText, Drawing.Color.Blue)
            End With
        End Set
    End Property

    Public Enum enumAutoReplaceDecimalSeparator
        ''' <summary>Only cells with single or double data type will do substitution</summary>
        Auto
        ''' <summary>All cells will do substitution</summary>
        AllCells
    End Enum

    ''' <summary>
    ''' Will decimal point/period character be replaced with culture-specific decimal separator
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>This allows users that have (for example) ES-CO culture setting which uses comma as decimal separator to have that automatically used when typing the decimal point</remarks>
    Public Property AutoReplaceDecimalSeparator As enumAutoReplaceDecimalSeparator = enumAutoReplaceDecimalSeparator.Auto

End Class

Public Class ContextMenuEventArgs
    Public Enum ContextMenuTypeEnum
        Fill
        Cut
        Copy
        Paste
        Delete
        DuplicateRow
        SelectAll
        Undo
        Find
        Replace
        Add
        Multiply
        Series
        AppendRows
        InsertRows
        DeleteRows
        ClearTable
        Print
        Stats
        PDF
        Excel
        Graph
    End Enum
    Public Cancel As Boolean   'should the command be cancelled?
    Public ContextMenuType As ContextMenuTypeEnum 'what menu was selected?
    Public RowCount As Integer 'the number of rows affected (if applicable)
    Public RowIndex As Integer 'the first row affected by the command
    Public Sub New(ByVal _ContextMenuType As ContextMenuTypeEnum)
        ContextMenuType = _ContextMenuType
    End Sub
    Public Sub New()
    End Sub
End Class

Public Class ProgressEventArgs
    Public CanCancel As Boolean 'allow user to cancel command?
    Public Cancel As Boolean   'has the command been cancelled?
    Public Percent As Integer  'percent complete
    Public Sub New(ByVal _Percent As Integer, ByVal _CanCancel As Boolean)
        Percent = _Percent
        CanCancel = _CanCancel
    End Sub
    Public Sub New()
    End Sub
End Class

#Region "Calendar Column & Cell"


Class CalendarEditingControl
    Inherits DateTimePicker
    Implements IDataGridViewEditingControl

    Private dataGridViewControl As DataGridView
    Private rowIndexNum As Integer
    Private valueIsChanged As Boolean = False

    Public Sub New()
        Me.Format = DateTimePickerFormat.Short
    End Sub

    Public Function EditingControlWantsInputKey(ByVal key As Keys, ByVal dataGridViewWantsInputKey As Boolean) As Boolean Implements IDataGridViewEditingControl.EditingControlWantsInputKey
        ' Let the DateTimePicker handle the keys listed.
        Select Case key AndAlso Keys.KeyCode
            Case Keys.Left, Keys.Up, Keys.Down, Keys.Right, _
                Keys.Home, Keys.End, Keys.PageDown, Keys.PageUp
                Return True
            Case Else
                Return False
        End Select
    End Function

    Public Function GetEditingControlFormattedValue(ByVal context As DataGridViewDataErrorContexts) As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
        Return Me.Value.ToShortDateString()
    End Function

    Public Sub ApplyCellStyleToEditingControl(ByVal dataGridViewCellStyle As DataGridViewCellStyle) Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
        Me.Font = dataGridViewCellStyle.Font
        Me.CalendarForeColor = dataGridViewCellStyle.ForeColor
        Me.CalendarMonthBackground = dataGridViewCellStyle.BackColor
    End Sub

    Protected Overrides Sub OnValueChanged(ByVal e As EventArgs)
        ' Notify the DataGridView that the contents of the cell have changed.
        valueIsChanged = True
        Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)
        MyBase.OnValueChanged(e)
    End Sub

    Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
        ' No preparation needs to be done.
    End Sub

    Public ReadOnly Property EditingControlCursor() As Cursor Implements IDataGridViewEditingControl.EditingPanelCursor
        Get
            Return MyBase.Cursor
        End Get
    End Property

    Public Property EditingControlDataGridView() As DataGridView Implements IDataGridViewEditingControl.EditingControlDataGridView
        Get
            Return dataGridViewControl
        End Get
        Set(ByVal value As DataGridView)
            dataGridViewControl = value
        End Set
    End Property

    Public Property EditingControlFormattedValue() As Object _
        Implements IDataGridViewEditingControl.EditingControlFormattedValue

        Get
            Return Me.Value.ToShortDateString()
        End Get

        Set(ByVal value As Object)
            If TypeOf value Is String Then
                Me.Value = DateTime.Parse(CStr(value))
            End If
        End Set

    End Property

    Public Property EditingControlRowIndex() As Integer Implements IDataGridViewEditingControl.EditingControlRowIndex
        Get
            Return rowIndexNum
        End Get
        Set(ByVal value As Integer)
            rowIndexNum = value
        End Set
    End Property

    Public Property EditingControlValueChanged() As Boolean Implements IDataGridViewEditingControl.EditingControlValueChanged
        Get
            Return valueIsChanged
        End Get
        Set(ByVal value As Boolean)
            valueIsChanged = value
        End Set
    End Property

    Public ReadOnly Property RepositionEditingControlOnValueChange() As Boolean Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
        Get
            Return False
        End Get
    End Property
End Class

Public Class DataGridViewCalendarCell
    Inherits DataGridViewTextBoxCell


    Public Sub New()
        ' Use the short date format.
        Me.Style.Format = MyDateFormat()
    End Sub

    Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, ByVal initialFormattedValue As Object, ByVal dataGridViewCellStyle As DataGridViewCellStyle)

        ' Set the value of the editing control to the current cell value.
        MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)

        Dim ctl As CalendarEditingControl = CType(DataGridView.EditingControl, CalendarEditingControl)
        ctl.Value = CType(Me.Value, DateTime)

    End Sub

    Public Overrides ReadOnly Property DefaultNewRowValue() As Object
        Get
            ' Use the current date and time as the default value.
            Return DateTime.Now
        End Get
    End Property

    Public Overrides ReadOnly Property EditType() As Type
        Get
            ' Return the type of the editing contol that CalendarCell uses.
            Return GetType(CalendarEditingControl)
        End Get
    End Property

    Public Overrides ReadOnly Property ValueType() As Type
        Get
            ' Return the type of the value that CalendarCell contains.
            Return GetType(DateTime)
        End Get
    End Property
End Class

Public Class DataGridViewCalendarColumn
    Inherits DataGridViewColumn


    Public Sub New()
        MyBase.New(New DataGridViewCalendarCell())
    End Sub

    Public Overrides Property CellTemplate() As DataGridViewCell
        Get
            Return MyBase.CellTemplate
        End Get
        Set(ByVal value As DataGridViewCell)

            ' Ensure that the cell used for the template is a CalendarCell.
            If (value IsNot Nothing) AndAlso Not value.GetType().IsAssignableFrom(GetType(DataGridViewCalendarCell)) Then
                Throw New InvalidCastException("Must be a CalendarCell")
            End If
            MyBase.CellTemplate = value

        End Set
    End Property
End Class
#End Region

#Region "ColorPickerColumn"


Public Class DataGridViewColorPickerCell
    Inherits DataGridViewComboBoxCell

    Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, ByVal initialFormattedValue As Object, ByVal dataGridViewCellStyle As System.Windows.Forms.DataGridViewCellStyle)
        Try
            MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
            Dim cbo As DataGridViewComboBoxEditingControl = MyBase.DataGridView.EditingControl
            cbo.Text = Me.Value
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Protected Overloads Overrides Sub Paint(ByVal graphics As Graphics, ByVal clipBounds As Rectangle, ByVal cellBounds As Rectangle, ByVal rowIndex As Integer, ByVal cellState As DataGridViewElementStates, ByVal value As Object, ByVal formattedValue As Object, ByVal errorText As String, ByVal cellStyle As DataGridViewCellStyle, ByVal advancedBorderStyle As DataGridViewAdvancedBorderStyle, ByVal paintParts As DataGridViewPaintParts)
        Try
            MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, DataGridViewPaintParts.All)
            If DataGridView Is Nothing OrElse value Is Nothing OrElse value.[GetType]() Is GetType(DBNull) Then Exit Sub
            If Not TypeOf value Is String Then Exit Sub
            'must make rectangle approximately correspond to the 'textbox' area to the left of the drop-down button (by inspection)
            Dim rect As Rectangle = New Rectangle(cellBounds.X + 3, cellBounds.Y + 3, cellBounds.Width - 26, cellBounds.Height - 8)
            Dim BlockColor As Color = Color.FromName(value)
            DataGridViewColorPickerEditor.DrawColorItem(graphics, rect, BlockColor, Me.DataGridView.Font)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Public Overrides ReadOnly Property EditType() As System.Type
        Get
            Return GetType(DataGridViewColorPickerEditor)
        End Get
    End Property
End Class

''' <summary>
''' This is a customization of the combobox column that shows standard colors with colored backgrounds; value of cell is name of color
''' </summary>
''' <remarks></remarks>
Public Class DataGridViewColorPickerColumn
    Inherits DataGridViewComboBoxColumn

    Private _SortByName As Boolean = False 'if false will sort by color
    Public Property SortByName As Boolean
        Set(value As Boolean)
            MyBase.Sorted = value
        End Set
        Get
            Return MyBase.Sorted
        End Get
    End Property

    ''' <summary>
    ''' Taken from http://www.codeproject.com/KB/selection/MultiTabColorPicker.aspx
    ''' See also: http://groups.google.com/group/microsoft.public.dotnet.framework.windowsforms.controls/browse_thread/thread/6c75aaa3bc23a954/4d6f4c0ff1583e1a?q=group:*dotnet*+%22sort+colors%22#4d6f4c0ff1583e1a
    ''' </summary>
    ''' <remarks></remarks>
    Private Class HSB_ColorComparer
        Implements IComparer(Of Color)

        Public Function Compare(ByVal c1 As Color, ByVal c2 As Color) As Integer Implements IComparer(Of System.Drawing.Color).Compare
            'If c1 = Color.Transparent Then
            '    Return -1
            'ElseIf c2 = Color.Transparent Then
            '    Return 1
            'End If

            If c1 = Color.Transparent And c2 = Color.Transparent Then
                Return 0
            ElseIf c1 = Color.Transparent Then
                Return -1
            ElseIf c2 = Color.Transparent Then
                Return 1
            End If

            If c1.GetHue() > c2.GetHue() Then
                Return 1
            ElseIf c1.GetHue() < c2.GetHue() Then
                Return -1
            Else
                If c1.GetSaturation() > c2.GetSaturation() Then
                    Return 1
                ElseIf c1.GetSaturation() < c2.GetSaturation() Then
                    Return -1
                Else
                    If c1.ToArgb() > c2.ToArgb() Then
                        Return 1
                    ElseIf c1.ToArgb() < c2.ToArgb() Then
                        Return -1
                    Else
                        Return 0
                    End If
                End If
            End If
        End Function

    End Class

    Private ColorCell As DataGridViewColorPickerCell

    Public Sub New()
        'add standard, non-system colors to sorted list
        ColorCell = New DataGridViewColorPickerCell
        MyBase.CellTemplate = ColorCell
        MyBase.Sorted = SortByName
        MyBase.Items.Clear()
        'Dim SysColors() As String = {"Active", "App", "Button", "Control", "Desktop", "Gradient", "GrayText", "Highlight", "Hot", "Inactive", "Info", "Menu", "Scroll", "Window"}
        'For Each c As String In [Enum].GetNames(GetType(KnownColor))
        '    Dim IsSys As Boolean = False
        '    For Each s As String In SysColors
        '        If c.StartsWith(s) Then IsSys = True : Exit For
        '    Next
        '    If Not IsSys Then Items.Add(c)
        'Next

        'instead of returning colors in alphabetical order, sort by similar colors
        For Each c As Drawing.Color In SortedColors()
            Items.Add(c.Name)
        Next
    End Sub

    ''' <summary>
    ''' Taken from http://www.codeproject.com/KB/selection/MultiTabColorPicker.aspx
    ''' </summary>
    ''' <returns>Sorted arraylist of colors in order of that used in VS</returns>
    ''' <remarks></remarks>
    Public Function SortedColors() As Generic.List(Of Drawing.Color)
        Try
            Dim lstColors As New Generic.List(Of System.Drawing.Color)
            Dim colorType As Type = GetType(System.Drawing.Color)
            For Each pi As System.Reflection.PropertyInfo In colorType.GetProperties(Reflection.BindingFlags.Public Or Reflection.BindingFlags.Static)
                If pi.PropertyType.Equals(colorType) Then
                    lstColors.Add(CType(pi.GetValue(colorType, Nothing), System.Drawing.Color))
                End If
            Next
            Dim HSB_Comparer As New HSB_ColorComparer
            lstColors.Sort(HSB_Comparer)
            Return lstColors
        Catch ex As Exception
            ErrorMsg(, ex)
            Return Nothing
        End Try
    End Function
End Class

Public Class DataGridViewColorPickerEditor
    Inherits DataGridViewComboBoxEditingControl

    Public Sub New()
        MyBase.New()
        MyBase.DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
    End Sub

    Friend Shared Sub DrawColorItem(ByVal g As Graphics, ByVal Rect As Rectangle, ByVal ItemColor As Color, ByVal ItemFont As Font)
        Try
            'draw rectangle filled with color; on top draw color name text, setting text color as black or white to give good contrast
            Dim sf As New StringFormat
            sf = New StringFormat()
            sf.LineAlignment = StringAlignment.Center
            sf.Alignment = StringAlignment.Center
            g.FillRectangle(New SolidBrush(IIf(ItemColor = Color.Transparent, Color.White, ItemColor)), Rect) 'or "transparent" shows through box
            g.DrawRectangle(Pens.Black, Rect)
            Dim b As Brush = Brushes.Black
            Dim DarkColors() As String = {"Black", "Blue", "Brown", "Crimson", "Dark", "Deep", "Dim", "Dodger", "Forest", "Fuchsia", "Gray", "Green", "Indigo", "Magenta", "Maroon", "Medium", "Midnight", "Navy", "Olive", "Orange", "Purple", "Red", "Rosy", "Royal", "SeaGreen", "Sienna", "Slate", "Steel", "Teal"}
            For Each s As String In DarkColors
                If ItemColor.Name.StartsWith(s) Then b = Brushes.White : Exit For
            Next
            g.DrawString(ItemColor.Name, ItemFont, b, Rect, sf)
            sf.Dispose()
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Protected Overrides Sub OnDrawItem(e As System.Windows.Forms.DrawItemEventArgs)
        Try
            MyBase.OnDrawItem(e)
            If e.Index = -1 Then Exit Sub
            Dim rect As New Rectangle(e.Bounds.X + 1, e.Bounds.Y + 1, e.Bounds.Width - 2, e.Bounds.Height - 2)
            Dim BlockColor As Color = Color.FromName(Items(e.Index))
            DrawColorItem(e.Graphics, rect, BlockColor, Me.Font)
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub
End Class
#End Region

#Region "Editable Combobox Column & Cell"

'Note: problem with this control: left/right cursor keys don't work nor does backspace, making it difficult to type/edit values

Public Class DataGridViewComboBoxEditCell
    Inherits DataGridViewComboBoxCell
    Private WithEvents dgv As DataGridView

    Private NewCellValue As String

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal ParentDataGridView As DataGridView)
        MyBase.New()
        dgv = ParentDataGridView
    End Sub

    Private Sub dgv_CellValidated(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles dgv.CellValidated
        Try
            If TypeOf dgv.CurrentCell Is DataGridViewComboBoxEditCell Then
                Dim cell As DataGridViewComboBoxCell = dgv.CurrentCell
                cell.Value = NewCellValue
            End If
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub dgv_CellValidating(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) Handles dgv.CellValidating
        Try
            If TypeOf dgv.CurrentCell Is DataGridViewComboBoxEditCell Then
                Dim cell As DataGridViewComboBoxCell = dgv.CurrentCell
                Dim val As String = e.FormattedValue

                'if value is not in list, add it
                If Not cell.Items.Contains(val) Then
                    cell.Items.Add(val)
                    cell.Value = val
                End If
                NewCellValue = val
            End If
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub

    Private Sub dgv_EditingControlShowing(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles dgv.EditingControlShowing
        Try
            If TypeOf dgv.CurrentCell Is DataGridViewComboBoxEditCell Then
                Dim cbo As DataGridViewComboBoxEditingControl = e.Control
                cbo.DropDownStyle = ComboBoxStyle.DropDown
                Dim cell As DataGridViewComboBoxCell = dgv.CurrentCell
            End If
        Catch ex As Exception
            ErrorMsg(, ex)
        End Try
    End Sub
End Class

Public Class DataGridViewComboBoxEditColumn
    Inherits DataGridViewColumn


    Public Sub New()
        MyBase.New(New DataGridViewComboBoxEditCell)
        'AddHandler MyBase.DataGridView.EditingControlShowing, AddressOf EditingControlShowing
    End Sub

    Private Sub cbo_Validating(ByVal sender As Object, ByVal e As DataGridViewRowCancelEventArgs)
        Dim cbo As DataGridViewComboBoxEditingControl = sender
        Dim dgv As DataGridView = cbo.EditingControlDataGridView
        If dgv.Columns(dgv.CurrentCellAddress.X) IsNot Me Then Exit Sub
        Dim val As String = cbo.Text

        'if value is not in list, add it
        If cbo.Items.IndexOf(val) = -1 Then
            cbo.Items.Add(val)
            Dim cbocol As DataGridViewComboBoxColumn = dgv.Columns(dgv.CurrentCell.ColumnIndex)
            cbocol.Items.Add(val)
            dgv.CurrentCell.Value = val
        End If
    End Sub

    Private Sub EditingControlShowing(ByVal Sender As Object, ByVal e As DataGridViewEditingControlShowingEventArgs)
        Dim cbo As DataGridViewComboBoxEditingControl = e.Control
        cbo.DropDownStyle = ComboBoxStyle.DropDown
        AddHandler cbo.Validating, AddressOf cbo_Validating
    End Sub
End Class
#End Region

Public Module ExtensionMethods
    <System.Runtime.CompilerServices.Extension> _
    Public Sub DoubleBuffered(dgv As DataGridView, setting As Boolean)
        Try

            Dim dgvType As Type = dgv.[GetType]()
            Dim pi As Reflection.PropertyInfo = dgvType.GetProperty("DoubleBuffered", Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic)
            pi.SetValue(dgv, setting, Nothing)


        Catch ex As Exception

            ErrorMsg(, ex)

        End Try

    End Sub
End Module